A simple payment method - part 2
In "A simple payment method" I implemented a really simple payment method. But there was one piece missing: If you created an actual order with the simple payment method you would see that the status of the payment would remain "pending". In this part, we will settle the actual payment (in a really simple way) and also implement a way to do a simple refund.
Before we do some coding, you need to know about an entity called SalesActivity
:
"A SalesActivity represents an instance of a charge or credit that occurs related to an Order.
When you provide your payment information and place the order, it is often the case that the actual charge to your payment instrument doesn't occur right away. We return back the Order confirmation and process the payment asynchronously (after validating in the PendingOrders minion, that the item can actually be delivered).
This may be due to buying a backorder or preorder or some other situation (fraud alert, inventory issue, etc) which causes you to delay settling the charge. When the order is settled, a SalesActivity is generated, representing that charge.
It is also often the case where multiple SalesActivities are generated during the lifecycle of an Order. For example, if the order is split and charges are done at two different times, a SalesActivity is generated, when each Order piece is fulfilled.
It is also possible to have a negative SalesActivity. If an Order is placed and then the customer subsequently returns all of some of the items in the order, when the customer is refunded, a SalesActivity with a negative number is generated.
These can be thought of as Debits and Credits in a Ledger and represents the history of charges/credits related to a specific order. The sum of the SalesActivities should match the GrandTotal of the Order."
(Source: https://sitecore.stackexchange.com/questions/14136/xc9-sales-activity-definiton)
About a sales activity
So, how is a SalesActivity
created? This is done, in the IPendingOrdersMinionPipeline
. It contains a block called CreateOrderSalesActivitiesBlock
which does the following for each payment method:
- It creates a
SalesActivity
entity; - It sets the
Amount
of the sales activity to the amount paid with that payment method; - It sets the
PaymentStatus
of the sales activity toPending
; - It adds the payment component from the order
- It adds the sales activity to a list of sales activities called
OrderSalesActivities
; - It adds the sales activity to the
SettleSalesActivities
list;
The SettleSalesActivities
list is monitored by the SettleSalesActivitiesMinion
. It runs every 5 minutes (of course you change that) and takes all the sales activities from the list and runs them through the ISettleSalesActivityPipeline
. If you need to check whether an order has been paid, this is the place to do it. The Braintree plugin adds two blocks to this pipeline to check the payment and update the order.
The last block in the ISettleSalesActivitiesPipeline
is the MoveAndPersistSalesActivityBlock
. It will move the SalesActivity to a different list based on it's status:
- Pending: sales activity remains on the
SettleSalesActivities
list; - Settled: sales activity moves to the
SettledSalesActivities
list; - Problem: sales activity moves to the
ProblemSalesActivities
list;
Note that in Sitecore Commerce 9.2, the Settlement Minion has been merged into the Released minion, to overcome risk of concurrency issues between the two minions. The release notes contain more information (look for TFS No. 325520)
Settling a simple payment
For my simple payment method I added a SettleSimplePaymentBlock
. It's Run
method looks like this:
public override Task<SalesActivity> Run(SalesActivity arg, CommercePipelineExecutionContext context)
{
Condition.Requires(arg).IsNotNull($"{this.Name}: The order cannot be null.");
var salesActivity = arg;
var knownSalesActivityStatuses = context.GetPolicy<KnownSalesActivityStatusesPolicy>();
if (!salesActivity.HasComponent<SimplePaymentComponent>()
|| !salesActivity.PaymentStatus.Equals(knownSalesActivityStatuses.Pending,
StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(salesActivity);
}
var payment = salesActivity.GetComponent<SimplePaymentComponent>();
// Perform logic to check whether the payment was settled
var settled = PaymentHasBeenSettled();
// Settle sales activity
if (settled)
{
context.Logger.LogInformation($"{this.Name} - Payment succeeded: {payment.Id}");
salesActivity.PaymentStatus = knownSalesActivityStatuses.Settled;
}
return Task.FromResult(salesActivity);
}
This code:
- First checks whether the sales activity is in the correct status (it should be pending);
- As I don't have anywhere to check whether the payment has been settled I randomly decide whether the payment has been settled or not;
- If payment has been settled, it will change the status to
Settled
;
The block is added to the ISettleSalesActivityPipeline
:
.ConfigurePipeline<ISettleSalesActivityPipeline>(c =>
c.Add<SettleSimplePaymentBlock>().Before<MoveAndPersistSalesActivityBlock>())
Note that the MoveAndPersistSalesActivityBlock
moves the sales activity from the SettleSalesActivities
list to the SettledSalesActivities
list (if the status is Settled
').
A simple refund
There will be situations where a customer returns all or part of an order. Sitecore Commerce has returns functionality out-of-the-box. Of course, you also need to return all or part of the payment.
Once the item has been received, the IRefundPaymentsPipeline
is run, so a refund can be started. For our simple payment method we are going to add a simple refund by adding a block to this pipeline:
public async override Task<OrderPaymentsArgument> Run(OrderPaymentsArgument arg,
CommercePipelineExecutionContext context)
{
Condition.Requires(arg).IsNotNull($"{this.Name} The arg can not be null");
Condition.Requires(arg.Order).IsNotNull($"{this.Name} The order can not be null");
Condition.Requires(arg.Payments).IsNotNull($"{this.Name} The payments can not be null");
var order = arg.Order;
if (!order.HasComponent<SimplePaymentComponent>())
{
return arg;
}
if (!order.Status.Equals(context.GetPolicy<KnownOrderStatusPolicy>().Completed,
StringComparison.OrdinalIgnoreCase))
{
var invalidOrderStateMessage = $"{this.Name}: Expected order in '{context.GetPolicy<KnownOrderStatusPolicy>().Completed}' status but order was in '{order.Status}' status";
await context.CommerceContext.AddMessage(
context.GetPolicy<KnownResultCodes>().ValidationError,
"InvalidOrderState",
new object[] { context.GetPolicy<KnownOrderStatusPolicy>().Completed, order.Status },
invalidOrderStateMessage);
return null;
}
var existingPayment = order.GetComponent<SimplePaymentComponent>();
var paymentToRefund = arg.Payments.FirstOrDefault(p =>
p.Id.Equals(existingPayment.Id, StringComparison.OrdinalIgnoreCase)) as SimplePaymentComponent;
if (paymentToRefund == null)
{
return arg;
}
if (existingPayment.Amount.Amount < paymentToRefund.Amount.Amount)
{
await context.CommerceContext.AddMessage(
context.GetPolicy<KnownResultCodes>().Error,
"IllegalRefundOperation",
new object[] { order.Id, existingPayment.Id },
"Order Simple Payment amount is less than refund amount");
return null;
}
// Perform logic to reverse the actual payment
if (existingPayment.Amount.Amount == paymentToRefund.Amount.Amount)
{
// Remove the existingPayment from the order since the entire amount is refunded
order.Components.Remove(existingPayment);
}
else
{
// Reduce the existing existingPayment in the order
existingPayment.Amount.Amount -= paymentToRefund.Amount.Amount;
}
await this.GenerateSalesActivity(order, existingPayment, paymentToRefund, context);
return arg;
}
This block does the following:
- It checks whether the order has a
SimplePaymentComponent
. If not, the block doesn't have to do anything and just returns; - Next, it checks whether the order is in the correct state (Completed);
- It checks if the amount to refund is equal to or smaller than the actual amount paid. If it's not, it will report an error.
- It then retrieves the existing payment, retrieves the payment to refund and then does the actual refunding. In our simple refund example, this just consists of creating a sales activity to note the refund and changing the amount of the original payment, but you can imagine in other scenarios you will order a payment provider to do a full or partial refund;
The block is added to the IRefundPaymentsPipeline
:
.ConfigurePipeline<IRefundPaymentsPipeline>(c =>
c.Add<RefundSimplePaymentBlock>().Before<PersistOrderBlock>())
A simple conclusion
This blog post and the previous one show you how to create a new payment method in Sitecore Commerce. Admittedly, it's a really simple one and your own implementations will probably be more complicated if you have to talk to a payment provider, but I hope these two blog posts give you the basis to successfully implement your own!
You can find the source code of this blog post on GitHub: https://github.com/ewerkman/Commerce.SimplePayment