This section contains instructions for further development and customisation of Klarna Checkout.
The code examples below are based on the Litium Accelerator B2C 5.5 MVC version. If you are using a different version of the accelerator, you may need to change the source accordingly.
Content
Modify data sent to Klarna
Enable to sell in multiple countries
B2B
Add product URLs and images
Klarna Checkout Remarketing
Push notification and order placement
Orders with Pending state
Validation callback
Troubleshooting
You can change the data sent to Klarna when the order is created and updated. The communication to Klarna from Litium uses the class Klarna.Rest.Models.CheckoutOrderData. The instance of this class is available from the method implementation in Src\Litium.Accelerator.Mvc\App_Start\KlarnaPaymentWidgetConfigV3.cs shown below.
- UpdateDataSentToKlarna: Called when the information is sent to Klarna. You can use this method to add or modify any changes made by Klarna that are not supported by Litium by default.
- Check the Klarna documentation for information on what options can be customised.
- Note that the changes are actually made in Klarna through the API, not in the Litium Klarna add-on.
public void UpdateDataSentToKlarna(OrderCarrier orderCarrier, KlarnaPaymentArgs paymentArgs, CheckoutOrderData klarnaCheckoutOrder)
{
//if the project has specific data to be sent to Klarna, outside the order carrier,
//or has them as additional order info, and is not handled by the Klarna addOn, modify the klarnaCheckoutOrder parameter.
//it is the klarnaCheckoutOrder object that will be sent to Klarna checkout api at Klarna.
//the format of klarnaCheckoutOrder parameter is described in Klarna API documentation https://developers.klarna.com.
//Set the checkout options here.
klarnaCheckoutOrder.Options = new CheckoutOptions
{
AllowSeparateShippingAddress = true,
ColorButton = "#ff69b4",
DateOfBirthMandatory = true
};
//External payment methods should be configured for each Merchant account by Klarna before they are used.
AddCashOnDeliveryExternalPaymentMethod(klarnaCheckoutOrder);
//to allow unrestricted shipping
}
You can enable to sell for multiple countries, which results in a country drop-down in the Klarna Checkout iframe. Send the ShippingCountries list (ISO Two letter country codes); see example below klarnaCheckoutOrder.ShippingCountries property. The same method above is modified as below for example to have UK and US in the list.
Note that Klarna need to configure the merchant account to allow this functionality.
public void UpdateDataSentToKlarna(OrderCarrier orderCarrier, KlarnaPaymentArgs paymentArgs, CheckoutOrderData klarnaCheckoutOrder)
{
//if the project has specific data to be sent to Klarna, outside the order carrier,
//or has them as additional order info, and is not handled by the Klarna addOn, modify the klarnaCheckoutOrder parameter.
//it is the klarnaCheckoutOrder object that will be sent to Klarna checkout api at Klarna.
//the format of klarnaCheckoutOrder parameter is described in Klarna API documentation https://developers.klarna.com.
//Set the checkout options here.
klarnaCheckoutOrder.Options = new CheckoutOptions
{
AllowSeparateShippingAddress = true,
ColorButton = "#ff69b4",
DateOfBirthMandatory = true
};
//External payment methods should be configured for each Merchant account by Klarna before they are used.
AddCashOnDeliveryExternalPaymentMethod(klarnaCheckoutOrder);
//to allow shipping contries.
//AllowSeparateShippingAddress may need to be set to true.
klarnaCheckoutOrder.ShippingCountries = new List<string>(){ "UK","US" };
}
Back to the top
To enable business customers to buy from Klarna checkout, it need to be enabled first for your Klarna merchant account. Then, modify the checkout options as shown below with AllowedCustomerTypes.
public void UpdateDataSentToKlarna(UrlHelper urlHelper, OrderCarrier orderCarrier, KlarnaPaymentArgs paymentArgs, CheckoutOrder klarnaCheckoutOrder)
{
//if the project has specific data to be sent to Klarna, outside the order carrier,
//or has them as additional order info, and is not handled by the Klarna addOn, modify the klarnaCheckoutOrder parameter.
//it is the klarnaCheckoutOrder object that will be sent to Klarna checkout api at Klarna.
//the format of klarnaCheckoutOrder parameter is described in Klarna API documentation https://developers.klarna.com.
//Set the checkout options here.
klarnaCheckoutOrder.CheckoutOptions = new CheckoutOptions
{
AllowedCustomerTypes = new List<string> { "person", "organization" }
};
}
Back to the top
It's possible to include product URLs and images in the data sent to Klarna, so they can display that information in their communication. You can achieve this by modifying the checkout options as below.
public void UpdateDataSentToKlarna(UrlHelper urlHelper, OrderCarrier orderCarrier, KlarnaPaymentArgs paymentArgs, CheckoutOrder klarnaCheckoutOrder)
{
//if the project has specific data to be sent to Klarna, outside the order carrier,
//or has them as additional order info, and is not handled by the Klarna addOn, modify the klarnaCheckoutOrder parameter.
//it is the klarnaCheckoutOrder object that will be sent to Klarna checkout api at Klarna.
//the format of klarnaCheckoutOrder parameter is described in Klarna API documentation https://developers.klarna.com.
//Add image and product URLs to all order lines
var channel = _channelService.Get(orderCarrier.ChannelID);
var channelUrl = _urlService.GetUrl(channel, new ChannelUrlArgs { AbsoluteUrl = true });
foreach (var orderLine in klarnaCheckoutOrder.OrderLines)
{
var variant = _variantService.Get(orderLine.Reference);
var productModel = _productModelBuilder.BuildFromVariant(variant);
var imageModel = productModel.GetValue<IList<Guid>>(SystemFieldDefinitionConstants.Images)?.FirstOrDefault().MapTo<ImageModel>();
orderLine.ImageUrl = channelUrl + imageModel.GetUrlToImage(Size.Empty, new Size(800, -1))?.Url; // customize size here
orderLine.ProductUrl = _urlService.GetUrl(variant, new ProductUrlArgs(channel.SystemId) { AbsoluteUrl = true });
}
}
Back to the top
If Klarna updates or add new properties for customizing the iframe, and CheckoutOptions class doesn't have these properties, you can override CheckoutOptions class to send the new properties.
Check the Klarna documentation to get the properties you want to extend.
See the example below which add 2 properties to CheckoutOptions.
public class ExtendedCheckoutOptions : CheckoutOptions
{
[JsonProperty(PropertyName = "vat_removed")]
public bool VatRemoved { get; set; }
[JsonProperty(PropertyName = "phone_mandatory")]
public bool PhoneMandatory { get; set; }
}
public void UpdateDataSentToKlarna(UrlHelper urlHelper, OrderCarrier orderCarrier, KlarnaPaymentArgs paymentArgs, CheckoutOrder klarnaCheckoutOrder)
{
//if the project has specific data to be sent to Klarna, outside the order carrier,
//or has them as additional order info, and is not handled by the Klarna addOn, modify the klarnaCheckoutOrder parameter.
//it is the klarnaCheckoutOrder object that will be sent to Klarna checkout api at Klarna.
//the format of klarnaCheckoutOrder parameter is described in Klarna API documentation https://developers.klarna.com.
//Set the checkout options here.
klarnaCheckoutOrder.CheckoutOptions = new ExtendedCheckoutOptions
{
AllowSeparateShippingAddress = true,
ColorButton = "#ff69b4",
DateOfBirthMandatory = true,
VatRemoved = true,
PhoneMandatory = true,
};
//External payment methods should be configured for each Merchant account by Klarna before they are used.
AddCashOnDeliveryExternalPaymentMethod(urlHelper, orderCarrier, klarnaCheckoutOrder);
}
Back to the top
Remarketing means sending an e-mail to users who have abandoned their shopping carts, so that they can proceed with the purchase. You can find more information on the Klarna developer site.
This feature is implemented in the add-on, and the calling code is in \src\litium.studio.accelerator\ecommerce\remarketing.cs.
To run the feature, add the Remarketing class as a scheduled task to your web.config file.
<scheduledTask type="Litium.Studio.Accelerator.ECommerce.Remarketing, Litium.Studio.Accelerator" startTime="00:30" interval="1d" parameters="" />
The following code skeleton is an example of how you can implement the remarketing feature in the Klarna add-on. The implementation uses a background task to find the abandonded orders and sends a mail to the recipient. The example does not include filtering of the abandoned carts if the customer has placed the same order later or with another device, and it does not contain the actual e-mail body text.
using System;
using System.Linq;
using Litium.Accelerator.CMS.Settings;
using Litium.Accelerator.Constants;
using Litium.AddOns.Klarna;
using Litium.AddOns.Klarna.ExtensionMethods;
using Litium.AddOns.Klarna.Kco;
using Litium.Foundation.Modules.CMS.Pages;
using Litium.Foundation.Modules.ECommerce;
using Litium.Foundation.Modules.ECommerce.Carriers;
using Litium.Foundation.Security;
using Litium.Foundation.Tasks;
namespace Litium.Accelerator.ECommerce
{
public class Remarketing : ITask
{
/// <summary>
/// Find abondoned carts, and send a re-marketing email to the customers.
/// Note: As at Jan 2017, only Klarna Checkout supports re-marketing., project need to implement the abondoned cart
/// email sent.
/// </summary>
/// <param name="token">Security token.</param>
/// <param name="parameters">Parameters or null.</param>
/// <exception cref="NotImplementedException"></exception>
public void ExecuteTask(SecurityToken token, string parameters)
{
// only Klarna checkout supports obtaining abondoned carts and re-marketing.
// project may implement custom solutions for other payment providers.
// find all payment methods for the Klarna V3 and try to find abandon carts.
var paymentMethods = ModuleECommerce.Instance.PaymentMethods.GetAll().Where(x => x.PaymentProviderName == KlarnaProvider.ProviderName).Select(x => x.Name);
foreach (var paymentMethod in paymentMethods)
{
var paymentAccountId = GetPaymentAccountId(paymentMethod);
if (string.IsNullOrEmpty(paymentAccountId))
{
continue;
}
var kcoApi = LitiumKcoApi.CreateFrom(paymentAccountId);
var abandonedCarts = kcoApi.GetAbandonedCarts();
foreach (var cart in abandonedCarts)
{
var toEmail = cart.OrderCarrier.CustomerInfo.Address.Email;
if (string.IsNullOrEmpty(toEmail))
{
continue;
}
// The abandonedCarts received above, may also have users who have successfully placed orders with a different session.
// Based on merchant requirements, filter out similar orders by looking at users order history.
// Alternative, specify in the email to customer, "if they have already placed the order, they can ignore the email".
// TODO: Add logic for filtering already placed orders
var url = GetCheckoutPageUrl(cart.OrderCarrier);
if (string.IsNullOrEmpty(url))
{
continue;
}
// Create email that should be sent to customer.
var subject = "Did you forgot your products?";
var body = $"TODO: Place the message to customer here!. Following link should carry the customer to his abandoned cart. <a href=\"{url}\" >Go to cart</a>";
// send email
// TODO: Add logic to send email.
}
}
}
private string GetCheckoutPageUrl(OrderCarrier orderCarrier)
{
var websiteId = orderCarrier.WebSiteID;
var checkoutPageId = WebsiteSettings.GetInstance(websiteId).CheckOutPageId;
var url = Page.GetUrlToPage(checkoutPageId, Guid.Empty, false);
var paymentInfo = orderCarrier.PaymentInfo.FirstOrDefault(x => x.PaymentProvider == KlarnaProvider.ProviderName);
var klarnaId = paymentInfo?.GetKlarnaOrderId();
if (string.IsNullOrEmpty(klarnaId))
{
return null;
}
var paymentAccountId = GetPaymentAccountId(paymentInfo.PaymentMethod);
if (string.IsNullOrEmpty(paymentAccountId))
{
return null;
}
return url + $"?{CheckoutConstants.QueryStringStep}={CheckoutConstants.Remarketing}&{CheckoutConstants.AccountId}={paymentAccountId}&{CheckoutConstants.TransactionNumber}={klarnaId}";
}
private static string GetPaymentAccountId(string paymentMethod)
{
var paymentMethodParts = paymentMethod.Split(' ');
if (paymentMethodParts.Length > 0)
{
return paymentMethodParts[0];
}
return null;
}
}
}
Back to the top
When the user places the order on the checkout page, the order is completed and Klarna calls the Litium server using a separate HTTP Post background call. This is called a push notification and takes place in another session than the checkout page.
[HttpPost]
public ActionResult PushNotification(string accountId, string transactionNumber)
{
var klarnaCheckout = CreateKlarnaCheckoutApi(accountId);
var checkoutOrder = klarnaCheckout.FetchKcoOrder(transactionNumber);
//to place the order, we need both the order carrier and the klarna checkout order.
if (checkoutOrder?.OrderCarrier != null)
{
PlaceOrder(klarnaCheckout, checkoutOrder);
}
return Content("OK");
}
At the same time a normal redirect callback is made when the confirmation is sent from Klarna.
[HttpGet]
public ActionResult Confirmation(string redirectUrl, string accountId, string transactionNumber)
{
var klarnaCheckout = CreateKlarnaCheckoutApi(accountId);
var checkoutOrder = klarnaCheckout.FetchKcoOrder(transactionNumber.Split('/').Last());
if (checkoutOrder != null)
{
PlaceOrder(klarnaCheckout, checkoutOrder);
return Redirect(redirectUrl);
}
return Content("Order not found!");
}
The PlaceOrder method can be called by both Confirmation and PushNotification. To avoid duplicate orders use the distributed lock in the PlaceOrder method, so that the order is only placed once.
Note: See how the distributed lock is taken, and also even if the order is present, we still check the payment status and call the ExecutePayment if the payment status is still Init, ExecuteReserve or Pending.
private void PlaceOrder([NotNull] ILitiumKcoApi klarnaCheckout, [NotNull] ILitiumKcoOrder checkoutOrder)
{
if (checkoutOrder.KlarnaOrderStatus == KlarnaOrderStatus.Complete)
{
using (new DistributedLock(checkoutOrder))
{
//there is a time gap between when we last fetched order from Klarna to the time of aquiring the distributed lock.
//just before the distributed lock was taken by this machine (but after this machine had fetched checkout order from klarna),
//another machine in the server farm may have got the lock, and created the order and exited releasing the distributed lock.
//That would cause this machine to aquire the lock, but we cannot use the existing checkoutorder because its out-dated.
//therefore we have to refetch it one more time!.
checkoutOrder = klarnaCheckout.FetchKcoOrder(checkoutOrder.OrderCarrier);
if (checkoutOrder != null && checkoutOrder.KlarnaOrderStatus == KlarnaOrderStatus.Complete)
{
//replace the order carrier in the session from the one received from Klarna plugin.
_cart.OrderCarrier = checkoutOrder.OrderCarrier;
//save the order only if it is not saved already.
var order = string.IsNullOrEmpty(checkoutOrder.OrderCarrier.ExternalOrderID)
? _moduleECommerce.Orders.GetOrder(checkoutOrder.OrderCarrier.ID, _securityToken)
: _moduleECommerce.Orders.GetOrder(checkoutOrder.OrderCarrier.ExternalOrderID, _securityToken);
if (order == null)
{
//execute the payment.
PaymentInfo[] paymentInfos;
order = _cart.PlaceOrder(_securityToken, out paymentInfos);
var paymentInfo = paymentInfos.FirstOrDefault();
try
{
paymentInfo?.ExecutePayment(_cart.CheckoutFlowInfo, _securityToken);
}
catch(Exception ex)
{
this.Log().Error("ExecutePayment failed",ex);
}
//now the order carrier has changed, refetch the checkoutOrder with updated data.
checkoutOrder = klarnaCheckout.FetchKcoOrder(order.GetAsCarrier(true, true, true, true, true, true));
_cart.OrderCarrier = checkoutOrder.OrderCarrier;
}
else
{
var paymentInfo = order.PaymentInfo.Where(x => x.PaymentProviderName == KlarnaV2.KlarnaProvider.ProviderName
|| x.PaymentProviderName == KlarnaV3.KlarnaProvider.ProviderName).FirstOrDefault();
try
{
//If the Litium call to Klarna to "notify order created" fails, the payment will be in following states.
//so execute payment need to be called again, to notify klarna of Litium order id.
if (paymentInfo.PaymentStatus == PaymentStatus.Init
|| paymentInfo.PaymentStatus == PaymentStatus.ExecuteReserve
|| paymentInfo.PaymentStatus == PaymentStatus.Pending)
{
paymentInfo?.ExecutePayment(_cart.CheckoutFlowInfo, _securityToken);
}
}
catch (Exception ex)
{
this.Log().Error("ExecutePayment failed", ex);
}
_targetGroupEngine.Process(new OrderEvent { Order = order });
}
}
}
}
}
Back to the top
In certain cases, Klarna will set the order as Pending when sending it back to Litium. This might be because they are making a credit check, or checking if the buyer actually has placed the order. Even if the state Pending is sent back from Klarna the order is created in Litium, which means that there will be another push notification at a later stage that will update the payment status in Litium.
Pending orders will get the payment status Pending and the order status Confirmed in Litium.
Back to the top
If the user account is changed on the checkout page, Klarna will call Litium to try to verify the change. The callback is executed when the user clicks Continue, and will only work if the Litium server has a public URL. If the Litium server does not have a public URL the checkout will fail.
Note: The error occurs within the Klarna iFrame, so there will be no entries about it in the Litium web log.
If you are using a private server in a testing scenario you need to comment out the address validation line in KlarnaPaymentWidgetController.cs like in the example below.
private ExecutePaymentArgs CreatePaymentArgs(OrderCarrier order, string paymentAccountId, Controller controller)
{
var checkOutPageId = WebsiteSettings.GetInstance(order.WebSiteID).CheckOutPageId;
var checkoutPage = Page.GetPage(checkOutPageId, _securityToken);
var pageDefinition = checkoutPage.As<CheckOutB2C>();
var protocol = _routePath.IsSecureConnection ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
if (!_routePath.IsSecureConnection)
{
this.Log().Trace("Klarna Checkout Validation is disabled. To enable the validate you need to use https on the checkout page.");
}
var checkoutFlowInfo = _cart.CheckoutFlowInfo;
checkoutFlowInfo.ExecutePaymentMode = ExecutePaymentMode.Reserve;
checkoutFlowInfo.RequireConsumerConfirm = false;
var checkoutPageUrl = Page.GetUrlToPage(checkOutPageId, Guid.Empty, false);
checkoutFlowInfo.SetValue(ConstantsAndKeys.TermsUrlKey, Page.GetUrlToPage(pageDefinition.TermsAndAgreementLink, Guid.Empty, false));
checkoutFlowInfo.SetValue(ConstantsAndKeys.CheckoutUrlKey, checkoutPageUrl);
var confirmationUrl = controller.Url.Action(nameof(Confirmation), "KlarnaPaymentWidget", new { RedirectUrl = Page.GetUrlToPage(checkoutPage.ID, checkoutPage.WebSiteID, false) }, Uri.UriSchemeHttps);
checkoutFlowInfo.SetValue(ConstantsAndKeys.ConfirmationUrlKey, confirmationUrl);
checkoutFlowInfo.SetValue(ConstantsAndKeys.PushUrlKey, controller.Url.Action(nameof(PushNotification), "KlarnaPaymentWidget", null, Uri.UriSchemeHttps));
checkoutFlowInfo.SetValue(ConstantsAndKeys.ValidationUrlKey, controller.Url.Action(nameof(Validate), "KlarnaPaymentWidget", null, Uri.UriSchemeHttps));
//checkoutFlowInfo.SetValue(ConstantsAndKeys.AddressUpdateUrlKey, controller.Url.Action(nameof(AddressUpdate), "KlarnaPaymentWidget", null, Uri.UriSchemeHttps));
return new KlarnaPaymentArgsCreator().CreatePaymentArgs(checkoutFlowInfo);
}
Back to the top