Sitecore Commerce comes with two payment methods out of the box: Federated Payment using Braintree as the payment provider and a gift card payment method. During development you often have a need for a payment method for which you don't need to enter any information and that just allows you to save the cart as an order. In this post I'm going to create the simplest payment method I could create that can be processed by the commerce engine. Note that this doesn't involve any third parties so there are no connections to payment providers, just adding a payment method to a cart so you can save it as an order.
You can find all the code on Github: https://github.com/ewerkman/Commerce.SimplePayment
Adding a payment method to the Sitecore Commerce Control panel
In Sitecore Commerce Control Panel, go to: Sitecore > Commerce > Commerce Control Panel > Shared Settings > Payment Options and add a new payment option called Simple. Note the Item ID of the newly created item, you're going to need it later.
Next go to your Storefront settings (Sitecore > Commerce > Commerce Control Panel > Storefront Settings > Storefronts > Your storefront > Payment Configuration and add the newly created payment method under Payment Options. Save it.
The engine part
Let's start with extending the engine. So how are payment methods processed by the engine?
Some things to know:
- You can add multiple payment methods to a cart. So you can add a credit card payment and a gift card payment to a cart.
- You add a payment to cart by adding a component that inherits from
Sitecore.Commerce.Plugin.Payments.PaymentComponent
. It has two properties you need to set:Amount
andPaymentMethod
.Amount
is of typeMoney
and is the amount paid for this method.PaymentMethod
is anEntityReference
. ThePaymentMethod
references the ID of the Payment Method you define in the Sitecore Commerce Control Panel. - When you turn a cart into an order, the out-of-the-box engine takes all the payment methods, adds up all the paid amounts and checks if it is equal to the total amount of the order. If it's not, the cart is not turned into an order.
Coding
For the engine I created a new plugin. Let's go through it:
SimplePaymentComponent
The SimplePaymentComponent
inherits from PaymentComponent
and, because we ant to keep it simple, is an empty class.
public class SimplePaymentComponent : PaymentComponent
{
}
CommandsController
A CommandsController
class that inherits from CommerceController
and contains one method called AddSimplePayment
that takes a cart and a payment as it's input.
[HttpPut]
[Route("AddSimplePayment()")]
public async Task<IActionResult> AddSimplePayment([FromBody] ODataActionParameters value)
{
if (!this.ModelState.IsValid || value == null)
{
return (IActionResult)new BadRequestObjectResult(this.ModelState);
}
if (!value.ContainsKey("cartId") ||
(string.IsNullOrEmpty(value["cartId"]?.ToString()) ||
!value.ContainsKey("payment")) ||
string.IsNullOrEmpty(value["payment"]?.ToString()))
{
return (IActionResult)new BadRequestObjectResult((object)value);
}
string cartId = value["cartId"].ToString();
var paymentComponent = JsonConvert.DeserializeObject<SimplePaymentComponent>(value["payment"].ToString());
var command = this.Command<AddPaymentsCommand>();
await command.Process(this.CurrentContext, cartId, new List<PaymentComponent> { paymentComponent });
return new ObjectResult(command);
}
This method:
- does some checking of it's input;
- retrieves the cart id;
- deserializes the
payment
part of it's input to aSimplePaymentComponent
instance; - executes the
AddPaymentsCommmand
to add the payments to the cart;
Configuration
Lastly, we need to configure the OData model by implementing a ConfigureServiceApiBlock
.
public override Task<ODataConventionModelBuilder> Run(ODataConventionModelBuilder modelBuilder, CommercePipelineExecutionContext context)
{
Condition.Requires(modelBuilder).IsNotNull($"{this.Name}: The argument cannot be null.");
modelBuilder.AddEntityType(typeof(SimplePaymentComponent));
ActionConfiguration addSimplePaymentConfiguration = modelBuilder.Action("AddSimplePayment");
addSimplePaymentConfiguration.Parameter<string>("cartId");
addSimplePaymentConfiguration.Parameter<SimplePaymentComponent>("payment");
addSimplePaymentConfiguration.ReturnsFromEntitySet<CommerceCommand>("Commands");
return Task.FromResult(modelBuilder);
}
And we implement ConfigureSitecore
to add this pipeline block to the ConfigureServiceApiPipeline
:
public class ConfigureSitecore : IConfigureSitecore
{
public void ConfigureServices(IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
services.RegisterAllPipelineBlocks(assembly);
services.Sitecore().Pipelines(config => config
.ConfigurePipeline<IConfigureServiceApiPipeline>(configure =>
configure.Add<ConfigureServiceApiBlock>())
);
services.RegisterAllCommands(assembly);
}
}
The website part
In the website part we create the code to integrate our new payment method into Commerce Connect. Let's start by creating the code to add a payment to the cart. This involves creating an instance of a class that inherits from PaymentInfo
and creating a request to add it to the cart:
var simplePaymentInfo = new SimplePaymentInfo();
simplePaymentInfo.PaymentMethodID = "D652AC0F-C832-450B-BBDD-2CBBDA95E3CC";
simplePaymentInfo.Amount = cart.Total.Amount;
simplePaymentInfo.PartyID = billingAddress.ExternalId;
var payments = new List<PaymentInfo>();
payments.Add(simplePaymentInfo);
var addPaymentInfoRequest = new AddPaymentInfoRequest(cart, payments);
var addPaymentInfoResult = cartServiceProvider.AddPaymentInfo(addPaymentInfoRequest);
What does SimplePaymentInfo
look like? Again, it's a simple class:
using Sitecore.Commerce.Entities.Carts;
namespace Web.SimplePayment.Entities
{
public class SimplePaymentInfo : PaymentInfo
{
public decimal Amount { get; set; }
}
}
If you run this code, you will see that you get an error when executing the cartServiceProvider.AddPaymentInfo(addPaymentInfoRequest);
code and the error indicates that SimplePaymentInfo is an unsupported payment type.
So, what happens under the covers is that Sitecore Commerce Connect executes the commerce.carts.addPaymentInfo
pipeline (in Sitecore XP). Out of the box this pipeline consists of one processor: Sitecore.Commerce.Engine.Connect.Pipelines.Carts.AddPaymentInfoToCart
.
If you take sneak dotPeek at the code for this processor, you will notice it only supports two payment methods: Federated and Giftcard. It even goes so far to declare an error if you added a different payment method.
We can handle our own payment method by adding a processor to this pipeline and handling any payment that is of type SimplePaymentInfo
.
The code for the processor that handles adding a simple payment method to the cart looks like this:
public class AddSimplePaymentToCart
{
public void Process(ServicePipelineArgs args)
{
AddPaymentInfoRequest request;
AddPaymentInfoResult result;
request = args.Request as AddPaymentInfoRequest;
result = args.Result as AddPaymentInfoResult;
if( request.Payments.Any(p => p is SimplePaymentInfo))
{
var cart = request.Cart;
var container = EngineConnectUtility.GetShopsContainer(shopName: cart.ShopName, customerId: cart.CustomerId);
foreach (var payment in request.Payments.Where(p => p is SimplePaymentInfo).Select(p => TranslatePayment((SimplePaymentInfo) p)).ToList())
{
var command = Proxy.DoCommand(container.AddSimplePayment(cart.ExternalId, payment));
result.HandleCommandMessages(command);
}
// Remove the SimplePaymentInfo payments from the list of payments, so they are not evaluated by other processors
request.Payments = request.Payments.Where(p => !(p is SimplePaymentInfo)).ToList();
}
}
private SimplePaymentComponent TranslatePayment(SimplePaymentInfo paymentInfo)
{
return new SimplePaymentComponent()
{
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
PaymentMethod = new EntityReference { EntityTarget = paymentInfo.PaymentMethodID },
Amount = Money.CreateMoney(paymentInfo.Amount)
};
}
}
First we check if there are any payment methods of the type we can handle (SimplePaymentInfo
) in the list of payment methods that is added.
if( request.Payments.Any(p => p is SimplePaymentInfo))
If there are, we loop through the payments of type SimplePaymentInfo
, translate each SimplePaymentInfo to a SimplePaymentComponent that Commerce Engine understands and send it to the engine, where it will be picked up by the CommandsController we created earlier.
Finally, we remove any payment from the list of payments so it's not processed by the other pipelines.
Configuring the pipeline
We add the processor created to the commerce.carts.addPaymentInfo
pipeline using the following configuration:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<commerce.carts.addPaymentInfo>
<processor type="Web.SimplePayment.Pipelines.AddSimplePaymentToCart,Web.SimplePayment"
patch:before="processor[@type='Sitecore.Commerce.Engine.Connect.Pipelines.Carts.AddPaymentInfoToCart, Sitecore.Commerce.Engine.Connect']" />
</commerce.carts.addPaymentInfo>
</pipelines>
</sitecore>
</configuration>
Adding the Simple payment method to your cart
You add the simple payment method to the cart using this code:
var simplePaymentInfo = new SimplePaymentInfo();
simplePaymentInfo.PaymentMethodID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; // Replace this with the id of the Simple payment method you added in the Commerce Control Panel. Be careful to REMOVE the { and }
simplePaymentInfo.Amount = cart.Total.Amount;
simplePaymentInfo.PartyID = billingAddress.ExternalId;
payments.Add(simplePaymentInfo);
var addPaymentInfoRequest = new AddPaymentInfoRequest(cart, payments);
var addPaymentInfoResult = cartServiceProvider.AddPaymentInfo(addPaymentInfoRequest);
The PaymentMethodID
should be set to ID the payment method you created earlier in the Sitecore Commerce Control Panel. When setting the PaymentMethodID
be careful not to include the {
and }
(I spent some time trying to figure out why my payment method wouldn't work).
Summary
This is the simplest payment method I could add. You are probably going to use a payment provider, in which case you can use the PaymentComponent to save specific information on the payment, like a transaction id etc.
(Don't forget to read the exciting next part: A Simple Payment Method - Part 2)