BlogCustom payment in Optimizely Commerce 14

Custom payment in Optimizely Commerce 14

OptmizelyCommerce.NET
1 November 2023

The Commerce platform offers a few of the most common payment methods. However, they are not always enough, and it is absolutely natural to need a fully customized approach.

Credit cards

Tokens everywhere

Especially when it comes to the credit card payment. Optimizely Commerce contains a Credit Card payment method, but it cannot be used anymore. It became obsolete and will be removed soon, as it does not meet PCI compliance. The problem as it seems, resits in storing credit card data in plain text.

The modern approach forces us to store credit card credentials in secure, fully cryptonized storage, which brings us to use external, specialized services. When digging into the Optimizely codebase, it can be found there is a built-in payment method that can perfectly be used here, named TokenizedPaymentMethod. However, at the time of writing this, it is noted that the new API is not stable.

Own solution

In this case, we can of course create our own payment. Unfortunately, it is not as easy as it seems. It requires a bit of research, taking of use other's experience and looking at the existing payment extensions available on public repositories like Foundation. This is the situation I have found myself in recently. This post describes what I found out and what I implemented.

What was needed to be done was a simple and widely known logic. Checkout page with payment form making it available to fill in credit card data which is charged when the order is submitted. So here we come with form. We collect data and send it to an external system like Stripe or Fortis to store it. In exchange, we are getting a token that makes it available to fetch data afterward. Data like the expiration date, the first six and last four digits of the credit card number. The token needs to be stored in Commerce - the most suitable place is the payment object.

Coding time

Firstly, a custom payment must be created. The class inherits the abstract Payment class and needs to be decorated with the [Serializable] attribute.

[Serializable]
public class TokenizedCreditCardPayment : Mediachase.Commerce.Orders.Payment
{
    public static Lazy<MetaClass> TokenizedCreditCardPaymentMetaClass => new(MetaClass.Load(OrderContext.MetaDataContext, TokenizedCreditCardPaymentMetaData.TokenizedPaymentMetaClassName));

    public TokenizedCreditCardPayment() : base(TokenizedCreditCardPaymentMetaClass.Value)
    {
        PaymentType = PaymentType.CreditCard;
        ImplementationClass = GetType().AssemblyQualifiedName;
    }

    protected TokenizedCreditCardPayment(SerializationInfo info, StreamingContext context) : base(info, context)
    {
        PaymentType = PaymentType.CreditCard;
        ImplementationClass = GetType().AssemblyQualifiedName;
    }

    public string CreditCardToken
    {
        get => GetString(TokenizedCreditCardPaymentMetaData.TokenizedCreditCardTokenMetaField);
        set => this[TokenizedCreditCardPaymentMetaData.TokenizedCreditCardTokenMetaField] = value;
    }
}
As it is shown, we reference MetaClass, and what is really important here is that MetaClass must be created first in the initialization module together with all fields we need - CreditCardToken in our case. It will not work without it, although the system will not complain during the execution. There is no exception indicating that a ghost MetaClass is used. Having payment initialized we can move forward and create a payment method.

The place where actual payment is created

It is implemented in quite a standard way that can be seen in the Foundation code base. In the CreatePayment method, we need to create our new payment object.

What is crucial here is the constructor. I noticed it is quite a standard when doing methods (this logic can be seen in almost every example). We have to find available methods configured in the commerce admin view for the current market, and if it can be found we need to assign MethodId or Name to the object.

public class TokenizedCreditCardPaymentMethod : IPaymentMethod
{
    public Guid PaymentMethodId { get; }
    public string Name { get; }
    public string Description { get; }
    public string SystemKeyword => "TokenizedCreditCard";

    public TokenizedCreditCardPaymentMethod(IPaymentMethodsService paymentMethodsService)
    {
        var availablePaymentMethods = paymentMethodsService?.GetAvailablePaymentMethods();
        var paymentMethod = availablePaymentMethods?.FirstOrDefault(x =>
            string.Equals(x.SystemKeyword, SystemKeyword, StringComparison.InvariantCultureIgnoreCase));

        if (paymentMethod == null)
        {
            return;
        }

        PaymentMethodId = paymentMethod.Id;
        Name = paymentMethod.Name;
        Description = paymentMethod.Description;
    }

    public bool ValidateData() => true;

    public IPayment CreatePayment(decimal amount, IOrderGroup orderGroup)
    {
        var payment = new TokenizedCreditCardPayment
        {
            PaymentMethodName = SystemKeyword,
            PaymentMethodId = PaymentMethodId,
            Amount = amount,
            TransactionType = TransactionType.Capture.ToString(),
            Status = PaymentStatus.Pending.ToString(),
            PaymentType = PaymentType.CreditCard
        };

        return payment;
    }
}

To get all available methods we need to use PaymentManager, which is a static class. It is not the best, so just to make it more flexible and testable, it is wrapped with IPaymentMethodsService.

Just one more thing

Before we can use our new payment flow, some configurations must be performed.

Firstly, we need to register our payment method in DI as any other service in .NET (services.addTransient<>), so we can load it whenever needed. For loading itself, a simple loader service can be implemented where we retrieve all registered methods and try to resolve available ones for specific scenarios. We can find by type or by method id.

internal class PaymentMethodLoader : IPaymentMethodLoader
{
    private readonly IEnumerable<IPaymentMethod> _paymentMethods;
    private readonly IPaymentMethodsService _paymentMethodsService;

    public PaymentMethodLoader(IEnumerable<IPaymentMethod> paymentMethods, IPaymentMethodsService paymentMethodsService)
    {
        _paymentMethods = paymentMethods;
        _paymentMethodsService = paymentMethodsService;
    }

    public T GetPaymentMethod<T>(string systemKeyword) where T : class, IPaymentMethod
    {
        var availableMethods = _paymentMethodsService.GetAvailablePaymentMethods();
        var resultMethodDefinition = availableMethods.FirstOrDefault(x => x.SystemKeyword == systemKeyword);
        if (resultMethodDefinition == null)
        {
            return default;
        }

        var resultMethod = _paymentMethods.FirstOrDefault(x => x.PaymentMethodId == resultMethodDefinition.Id);
        return resultMethod as T;
    }
}

The next step is to make our new method available. To do it, we need to run the website and go to the commerce admin where we can find the payment settings. We need to add a new payment method, make it active and available in our desired market.

Payment must be added in admin mode to be available for usage

Now we are ready to implement the flow. This can be fully customized. For example, we might have an API endpoint /checkout/payment where we can update payment. This API will then retrieve the payment method from the loader service, create a payment, add it to the cart/order, and save it.

What next?

Now we have it. The credit card token is stored in our order form inside the payment object. Thanks to that we can ask external service for more details about stored credit cards whenever needed and for example show limited credit card data on the checkout review step page.

More articles

Photo by Merakist on Unsplash
SEO.NET

SEO redirects in .NET

Nice and easy way to add necessary SEO redirects in .NET

Colorful lego bricks
.NETOptimizelyWeb DevelopmentDesign Patterns

Global Components Builders

Implementing global common components every site consists of

Have a question?

Don't hesitate, and send me an email

smutek.damian95@gmail.com