A major facelift for the Geta Klarna Checkout module
In this first blog post I'll go through how the module can be integrated on your checkout page. In the next upcoming blog post I will cover order management processes such as complete shipment with capture payment, cancel reservation and handling of returns.
What's new?
Here are some of the new features:
- Upgraded to latest Klarna API version (v3.0)
- Upgraded to EPiServer v9 and removed dependency on log4net
- Added settings as Commerce provider settings
- Added KlarnaCheckoutGateway in order to support standard commerce payment workflow
- Added api support for order management
- Capture on complete shipment (package is shipped)
- Cancel reservation if no items shipped
- Credit for handling returns
- Initializable module that adds necessary commerce meta fields
- IPostProcessPayment interface to support custom processing after ProcessPayment has completed
- Tax for line items and shipping
How to install it
The module consists of two nuget packages which are available on http://nuget.episerver.com/
The installation process is documented in detail on Github.
How to use it
Klarna Checkout requires four endpoints for checkout;
Checkout, Confirm, Push and Terms.
I will go through each one and show examples below.
Please note that in order to focus on the Klarna implementation common commerce parts, error handling and logging have been left out, see code comments for clearity.
[HttpGet]
public ActionResult Index(CheckoutPage currentPage)
{
// This is where you do your "normal" checkout stuff like
// initialize your model with shipment options, payment and cart items
// Run CartValidateActivityFlow once to calculate all taxes, charges and get the correct total amounts
}
The next step is the KlarnaCheckout action - this is where you have to call Klarna Checkout and provide all Klarna's cart items including shipping information, locale of Klarna Checkout, URL's of all endponts mentioned here. Klarna API will return HTML snippet which you have to render on your page where user will fill all required details for Klarna. Here is an example:
[HttpPost]
public ActionResult KlarnaCheckout(KlarnaCheckoutViewModel postedCheckoutViewModel)
{
// Here you should validate your cart, set shipment and more...
// Run the CartPrepareActivityFlow that associates line items with shipments in the OrderForm,
// calculates shipment prices, tax totals, order total
var model = ValidateModel(postedCheckoutViewModel);
var cart = GetCart();
var lineItems = cart.OrderForms.Any() ? cart.OrderForms.First().LineItems.ToList() : new List<LineItem>();
var cartItems = lineItems.Select(item => item.ToCartItem()).ToList();
var shipment = cart.OrderForms[0].Shipments.FirstOrDefault();
if (shipment == null)
{
throw new Exception("Shipment not selected. Shipment should be persisted into the cart before checkout.");
}
cartItems.Add(shipment.ToCartItem());
var baseUri = GetBaseUri();
var currentCheckoutPageUrl = string.Format("{0}{1}", baseUri, currentPage.PublicUrl());
var checkoutUris = new CheckoutUris(
new Uri(currentCheckoutPageUrl),
new Uri(currentCheckoutPageUrl + "KlarnaConfirm"),
new Uri(currentCheckoutPageUrl + "KlarnaPush"),
new Uri(currentCheckoutPageUrl + "KlarnaTerms"));
var response = CheckoutClient.Checkout(cartItems, PaymentSettings.CurrentLocale, checkoutUris);
model.KlarnaHtmlSnippet = response.Snippet;
model.KlarnaTransactionId = response.TransactionId;
cart.AcceptChanges(); //save shipment before returning view
return View(model);
}
Properties for accesssing payment settings (configured in commerce manager) and checkout client:
private ProviderSettings _providerSettings;
private ProviderSettings PaymentSettings
{
get
{
if (_providerSettings == null)
{
_providerSettings = PaymentHelper.GetProviderSettings(
_currentMarket, ShoppingContext.CurrentLanguageBranch);
}
return _providerSettings;
}
}
private static CheckoutClient _checkoutClient;
private CheckoutClient CheckoutClient
{
get
{
if (_checkoutClient == null)
{
var settings = PaymentSettings;
_checkoutClient = new CheckoutClient(settings.OrderBaseUri, settings.MerchantId,
settings.Secret, false, "#F50057");
}
return _checkoutClient;
}
}
The last parameter for the CheckoutClient constructor is optional and will override the default color scheme for Klarna. By using #F50057 I get a pink button:
When the user completes the purchase the Confirm action will be called with klarna order id as parameter.
public ActionResult KlarnaConfirm(CheckoutPage currentPage, string klarnaOrder)
{
var model = new KlarnaCheckoutViewModel(currentPage);
model.KlarnaTransactionId = klarnaOrder;
if (string.IsNullOrEmpty(klarnaOrder))
return RedirectToAction("Index");
//Before proceeding, make sure cart has not been manipulated
var klarnaOrderObject = CheckoutClient.GetOrder(klarnaOrder);
var cart = GetCart();
if (!IsPaymentValidForCart(klarnaOrderObject.TotalCost, cart))
{
Logger.WarnFormat("Cart has changed, cart total is {0}, Klarna total is {1}." +
"Redirecting back to checkout page.", cart.Total, ((decimal)klarnaOrderObject.TotalCost) / 100);
return RedirectToAction("Index");
}
model.OrderNumber = OrderNumberGenerator.GetOrderNumber(cart);
//rename cart and remove anonymous cookie
RenameCartAndSaveChanges(myCartHelper, model.KlarnaTransactionId,
ControllerContext.HttpContext.User.Identity, Response);
model.KlarnaHtmlSnippet = klarnaOrderObject.Snippet;
return View(model);
}
private bool IsPaymentValidForCart(int klarnaTotal, Cart cart)
{
decimal klarnaOrderCost = ((decimal)klarnaTotal) / 100;
return klarnaOrderCost == cart.Total;
}
It's important to verify that the cart hasn't changed after the user pressed "go to payment", hence the call to IsPaymentValidForCart. If the cart has changed the user is sent back to Index view to verify the items in the cart before proceeding to payment once more.
In the RenameCartAndSaveChanges I am renaming the cart using the Klarna order id, so that I can load it later when I get the Push call from Klarna.
Push is called from Klarna when order is confirmed, but status not updated to created. In the sample code for the Push method I have left out the "heavy stuff" which is creating the order in commerce, i.e. converting the cart to a purchase order. The ConfirmResponse object will contain all the data you need, data such as billing address and more. It's important that the payment object contains meta data for klarna order id and reservation number (see code below). The reservation number will be used for capturing payment when the order is shipped.
public ActionResult KlarnaPush(string klarnaOrder)
{
var response = CheckoutClient.Confirm(klarnaOrder);
// Order updated from status complete to created
if (response.Status == Geta.Klarna.Checkout.Models.OrderStatus.Created)
{
// NOTE: When creating the Payment object, make sure to save klarna order id and reservation number
// paymentObject.TransactionID = klarnaOrder;
// paymentObject[MetadataConstants.ReservationField] = response.ReservationNumber; PrepareOrder(response);
}
else
{
var msg =
string.Format(
"KLARNA PUSH FAILED: Status is is {0} for transaction {1}.",
response.Status, klarnaOrder);
Logger.ErrorFormat(msg);
}
return new HttpStatusCodeResult(HttpStatusCode.OK);
}
The last Klarna endpoint, Terms, is needed to display terms of your site in Klarna Checkout. It can be some MVC view or even static HTML file.
About taxes
In order to calulate tax percent, the implementation will look for a meta field "VatPercent" (MetadataConstants.VatPercent) on each line item. Tax for each line item is not calculated by default, so you will have to set the meta field value in a suitable work flow activity. Please see commerce tax documentation and the following blog post for more information. The custom meta field is automatically added to LineItemEx class as part of Commerce initialization, see MetadataInitialization.cs
What's next?
When the order is created (payment is authorized), a reservation is made in Klarnas system. This reservation will be made into an invoice when the order is shipped. The module also includes a payment gateway for handeling order management processes such as complete shipment with capture payment, cancel reservation and handling of returns. I will cover this in detail in an upcoming blog post.
To start testing you should apply for a Klarna test account, please see Klarnas developer documentation.
Please refer to Github documentation for how to configure the payment method inside Commerce Manager.
More about the module
Read the follow up blog post: Validating a checkout order.