This document is intended for developers that are new to Litium Commerce Cloud (LCC), it is recommended that you first read the Litium Commerce Cloud introduction to have a general understanding of what LCC is.
A license is not required to setup a Litium installation on your local computer, but without a License file Litium will limit number of requests per minute when the site is accessed from a machine other than localhost
.
When you deploy to test/stage/prod environments you will need a license-file:
The license must cover all environments (test/prod/stage) that customers have access to
Request a License file from https://docs.litium.com/support/request-license
Litium CDN Powered by Fastly handles all requests when the site has been deployed to Litium Cloud
Litium uses two client-side frameworks
Litium Accelerator is built as a Microsoft .NET MVC on .NET 6
The database used is Microsoft SQL Server
The search engine in the Accelerator is Litium search built on Elasticsearch
Redis in-memory data store is used for:
Automapper is used to map data between similar objects, mainly to map data from the general object to MVC ViewModels, example:
// 1. Register mapping:
cfg.CreateMap<PageModel, ArticleViewModel>()
.ForMember(x => x.Title, m => m.MapFromField(PageFieldNameConstants.Title))
.ForMember(x => x.Text, m => m.MapFromField(PageFieldNameConstants.Text))
.ForMember(x => x.Image, m => m.MapFrom<ImageModelResolver>());
// 2. Then use:
var articleViewModel = pageModel.MapTo<ArticleViewModel>();
All Litium objects share a common template system model called the Field framework. In this framework developers can create custom datatypes and field templates to represent different objects. An important task at the beginning of every project is to define the data models to use, and set which information needs to be stored for all entities (e.g. customers, pages or products).
All fields in the field framework can be localized, meaning that a value for the field can be set for all languages of the installation.
Not all objects in Litium have the field framework, those that do are:
Area | Entities |
---|---|
Media | Media object / Folder |
Customers | Person / Group / Organization |
Sales | - |
Products | Product / Category |
Website | Website / Page / Block |
Globalization | Market / Channel |
A fieldtype defines the datatype for a field (e.g. date, number or string), fieldtypes are global and all fieldtypes are avilable in all areas of the Litium installation. Developers can also create custom field types.
A field is an instance of a fieldtype (i.e. the field brand is of fieldtype text). Fields are created and scoped for a specific area.
Fields created by the Accelerator are defined in code, see folder \Src\Litium.Accelerator\Definitions\
in the Accelerator solution.
Example of a field defined in code:
new FieldDefinition<CustomerArea>("SocialSecurityNumber", SystemFieldTypeConstants.Text)
{
CanBeGridColumn = true, // Can be added as column to customer lists in Backoffice
CanBeGridFilter = true, // Can be used to filter customer lists in Backoffice
MultiCulture = false, // Should a field value be stored for every language?
}
As mentioned above the fieldtype defines the datatype for a field (e.g. date, number or string), but some field types do not only store data to be viewed, instead they have special functions or store references.
The pointer field type is used to select a specific entity, i.e. selecting a product for a news page or selecting a reseller organization for a product.
Example of a pointer field defined in code:
new FieldDefinition<BlockArea>(BlockFieldNameConstants.Link,
SystemFieldTypeConstants.Pointer)
{
Option = new PointerOption { EntityType = PointerTypeConstants.WebsitesPage }
}
The multifield acts as a container for other fields (multiple fields can be added).
The multifield is also one of few field types (option-fields can be set to multi-select) that can be defined as an array. This brings a lot of value when you develop custom field types since you never have to implement array support yourself (like field UI, serialization etc.) for arrays, if a user want to use it as an array they can just wrap it in a Multi field.
Say that you want to create a slideshow on the startpage, then you could create a multifield called Slide and add 3 fields to it:
By also defining the Slide-field as array, any number of slides can be added to the slideshow.
Example of the slide-multifield defined in code:
new FieldDefinition<BlockArea>(FieldConstants.Slide,
SystemFieldTypeConstants.MultiField)
{
Option = new MultiFieldOption {
IsArray = true,
Fields = new List<string>() {
FieldNameConstants.ImagePointer,
FieldNameConstants.LinkText,
FieldNameConstants.LinkToPage
}
}
}
All entities that use the field framework (e.g. person, group, product) are created using a field template. The field template is a definition of fields that are visible when the entity is edited in backoffice.
Note that the field template does not define the fields that can be added to an entity, only which fields are visible. The acutal data is always added to the entity and is not removed/changed if the entity switches template.
Fields in a field template are grouped together in field groups.
Example of a field defined in code:
new PersonFieldTemplate("MyPersonTemplate")
{
FieldGroups = new []
{
new FieldTemplateFieldGroup()
{
Id = "General",
Collapsed = false,
Fields =
{
SystemFieldDefinitionConstants.FirstName,
SystemFieldDefinitionConstants.LastName,
SystemFieldDefinitionConstants.Email,
"SocialSecurityNumber"
}
}
}
}
In Litium the entities product, category and page have landing pages. To display these landing pages you need to connect the field templates if these entities to MVC Controllers, this connection is the display template.
If you inspect the field template of the Article-page in the settings of Litium backoffice you can see that it specifies the Index
-action of the ArticleController
as display template.
The product area handles the display template connection slightly different. A product field template will not point directly to a MVC controller, instead it points to a display template-object. This display template can be shared between multiple field templates and it has some additional configuration options:
Using dependency injection in Litium is simple, all you need to to is add an attribute to an object and Litium will automatically register all instances of that object making it available for constructor injection.
The Litium platform separates abstractions (interfaces/abstract classes) from implementations in different assemblies. The IPriceCalculator
-interface is for example located in the Litium.Abstractions
assembly:
[Service(ServiceType = typeof (IPriceCalculator))]
[RequireServiceImplementation]
public interface IPriceCalculator
But its implementation is in the Litium.Application
assembly:
[Service(FallbackService = true)]
public class PriceCalculatorImpl : IPriceCalculator
FallbackService
Impl
-suffix and the fallback-attributeRequireServiceImplementation
-attribute on the interface prevents developers from adding a reference to the Abstraction without also referencing an Implementation (Litium will not start if an abstraction with this attribute is missing implementations).Say that you have an interface called IDessert
that has the Service
-attribute, any class that implements this interface will be automatically registered:
[Service(ServiceType = typeof(IDessert), Lifetime = DependencyLifetime.Singleton)]
public interface IDessert
{
void Serve();
}
The implementation may be placed anywhere in the solution (if there are multiple implementations you can use service factory or named service to instruct Litium on which implementation to resolve):
public class IceCream : IDessert
{
public void Serve()
{
// Serve some ice cream
}
}
Example on injecting the abstraction in a class constructor:
public class Waiter
{
public Waiter(IDessert dessert)
{
dessert.Serve();
}
}
The Service
-attribute of the IDessert
-interface above also has a Lifetime
-parameter (if not specified it will default to DependencyLifetime.Singleton
), available options are:
DependencyLifetime.Singleton
: All users will receive the same single instance from the container for the lifetime of the Litium applicationDependencyLifetime.Scoped
: The same instance is used within the current scope:
DependencyLifetime.Transient
: A new instance will be created each time the service is requested from the containerIn many cases you do not want to replace a class or interface entirely, perhaps you only need to modify input or output of a single method, then you can use a service decorator.
Using a service decorator could be described as getting the benefits of inheritance but at the same time the benefits of only referencing abstractions.
You can read more about service decorator on Litium docs but the best way to really understand the service decorator is to complete the service decorator development task on GitHub.
DependencyLifetime.Singleton
this data is only instanciated once and then re-used until Litium restarts. This is a big risk if the data need to be fresh (as with user data). This is also a risk if you use Scoped
or Transient
since another developer may inject and store your Transient
-class in their Singleton
, so try to keep your code stateless to minimize risk.Read more about service registration on Litium docs
This section will introduce you to some of Litiums common technical areas.
Use Litium.Scheduler.SchedulerService
to create jobs that run in the background.
Jobs are stored in the Database and will be executed even if the application restarts.
var fiveSecondsFromNow = DateTimeOffset.Now.AddSeconds(5);
_schedulerService.ScheduleJob<TestJobService>(
x => x.RunMyJob(), // <-- You can also pass parameters here!
new ScheduleJobArgs { ExecuteAt = fiveSecondsFromNow }
);
See MailServiceImpl
and PurchaseHistoryIndexDocumentBuilder
in the Accelerator for additional samples.
Implement the ICronScheduleJob
-interface to create a job that run at a set interval:
[CronScheduler("Litium.Accelerator.Services.RandomNumberLogger", DefaultCronExpression = CronEveryMinute)]
public class RandomNumberLogger : ICronScheduleJob
{
public const string CronEveryMinute = "0 0/1 * 1/1 * ? *";
private readonly ILogger<RandomNumberLogger> _logger;
public RandomNumberLogger(ILogger<RandomNumberLogger> logger)
{
_logger = logger;
}
public async ValueTask ExecuteAsync(object parameter, CancellationToken cancellationToken = new())
{
_logger.LogDebug($"Your random number this minute is: {new Random().Next(100)}");
}
}
When a scheduled job has been created it can be adjusted with new parameters and scheduling in appsettings.json
:
"Policy": {
"Litium.Accelerator.Services.RandomNumberLogger": { "CronExpression": "0/10 * * * * ?" }
Sometimes you need to run code every time the application starts, example:
To run code on startup just add the Autostart
-attribute:
[Autostart]
public class StartupLogger
{
public StartupLoggerDemo(ILogger<StartupLogger> logger)
{
logger.LogDebug("This code in the constructor will run every time Litium starts");
}
}
If your startup code takes time to run it will block and delay the startup, in this case it is better to run it asynchronously. Keep the AutoStart
-attribute and add the IAsyncAutostart
-interface to execute code in the StartAsync
-method:
[Autostart] // <-- Note that the autostart attribute is still required for IAsyncAutostart to work
public class AsyncStartupLogger : IAsyncAutostart
{
private readonly ILogger<AsyncStartupLogger> _logger;
public AsyncStartupLogger(ILogger<AsyncStartupLogger> logger)
{
_logger = logger;
_logger.LogDebug("This code will run synchronously (blocking) when litium starts");
}
public async ValueTask StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("This code will run asynchronously in the background (non-blocking) when litium starts");
}
}
In Litium all events are handled by Litium.Events.EventBroker
.
To create a custom event you implement the IMessage
-interface, and if you need to pass data also inherit from EventArgs<>
:
public class MyEvent : EventArgs<Currency>, IMessage
{
public MyEvent(Currency item) : base(item)
{
}
}
To publish (trigger) an event just constructor inject EventBroker
and use the Publish
-method:
public class MyEventPublisher
{
private readonly EventBroker _eventBroker;
public MyEventPublisher(EventBroker eventBroker)
{
_eventBroker = eventBroker;
}
public void SendEvent()
{
_eventBroker.Publish(new MyEvent(new Currency("SEK")));
}
}
To listen to an event just register a subscription with the EventBroker
. You can use AutoStart to make sure that the registration is done every time Litium re-starts:
public class MyEventSubscriber : IAsyncAutostart
{
private readonly IApplicationLifetime _applicationLifetime;
private readonly EventBroker _eventBroker;
private readonly ISubscription<MyEvent> _subscription;
public MyEventSubscriber(EventBroker eventBroker, IApplicationLifetime applicationLifetime)
{
_eventBroker = eventBroker;
_applicationLifetime = applicationLifetime;
}
public ValueTask StartAsync(CancellationToken cancellationToken)
{
var subscription = _eventBroker.Subscribe<MyEvent>(ev =>
{
// This code will execute every time MyEvent is published
var currency = ev.Item;
});
_applicationLifetime.ApplicationStopping.Register(() =>
{
// Application is about to shutdown, unregister the event listener
subscription.Dispose();
});
return ValueTask.CompletedTask;
}
}
Important notes on events:
In a load-balanced environment an event normally triggers only on the current server. To also trigger subscriptions on remote servers you need to adjust EventScope
when publishing the event. When using remote events Litium will use Service bus in the background to send the event.
The event system is used by Litium to update cache and search index when data is modified. You may modify the application database directly (at own risk!), but if you do you need to manually clear cache and rebuild indexes for changes to take effect in the UI.
Most events can be subscribed to from outside the Litium application using Webhooks, you can use Litium Swagger to get a list of the events that support webhooks.
SecurityContextService is used to get and set information about the current user context.
Check if the current user is logged in or not:
var currentUserIsLoggedIn = _securityContextService.GetIdentityUserSystemId().HasValue;
if(currentUserIsLoggedIn)
{
var personSystemId = _securityContextService.GetIdentityUserSystemId();
var currentlyLoggedInPerson = _personService.Get(personSystemId.Value);
}
Litium will always execute code with the permissions of the currently logged in user. There may be situations when you need to change this behaviour, for example during integrations that will need to write information even though the code is executing withouth authentication:
// Temporarily elevate permissions
// The system user account can perform any update in the api
using (_securityContextService.ActAsSystem("My custom integration task"))
{
// Do stuff here that the current user cannot normally do
}
// Temporarily remove permissions (execute code as anonymous user even if logged in)
using (_securityContextService.ActAsEveryone())
{
// Do stuff here that the need to be done without custom permissions
}
SecurityContextService
can also execute code as a specific person, this is safer than using system-privileges since executing code as a user having only the required permissions will prevent unintended changes:
if (person != null && !string.IsNullOrEmpty(person.LoginCredential.Username))
{
var claimsIdentity = _securityContextService.CreateClaimsIdentity(
person.LoginCredential.Username, person.SystemId);
using (_securityContextService.ActAs(claimsIdentity))
{
// Do stuff here as any provided person
}
}
You can validate updates to all entities in Litium with custom validations:
Implement the general IValidationRule
-interface to trigger a validation on all entity updates
An easier way is to inherit ValidationRuleBase<>
(that implements the interface) to trigger your validation only when a specific type changes, for example Currency
in this example:
public class ValidationExample : ValidationRuleBase<Currency>
{
public override ValidationResult Validate(Currency entity, ValidationMode validationMode, CultureInfo culture)
{
var result = new ValidationResult();
if(entity.TextFormat != "C0")
{
result.AddError("TextFormat", "Need to be C0")
}
return result;
}
}
Try the development task on GitHub to implement a custom validation.
Constructor inject ILogger<Type>
in a class to write to the application log, then write to the log with:
_logger.LogTrace("Creating visitor group");
\Src\Litium.Accelerator.Mvc\NLog.config
, for example to:
Entities (e.g. person, group or page) in Litium are accessed and modified using the entitys service.
Example:
public abstract class ChannelService : IEntityService<Channel>, IEntityAllService<Channel>
{
public abstract void Create(Channel channel);
public abstract void Delete(Channel channel);
public abstract Channel Get(Guid systemId);
public abstract IEnumerable<Channel> Get(IEnumerable<Guid> systemIds);
public abstract IEnumerable<Channel> GetAll();
public abstract void Update(Channel channel);
}
When an entity is retreived from Litium it is always in read-only mode (because cached entities should be non modifiable)
If you want to modify the entity you need to create a new writable version by calling MakeWritableClone()
then use the entity service to update, example:
person = person.MakeWritableClone();
person.LoginCredential.NewPassword = newPassword;
_personService.Update(person);
DataServcie
gives developers direct access to the Litium database, to query or modify data.
Try the data service development task on GitHub to learn more.
Use Litium.Data.DataService.CreateBatch
to modify data. CreateBatch
can be used to do multiple modifications in a single database transaction. If any part of the trancaction scope fail the entire transaction is rolled back making CreateBatch
ideal for dependent modifications.
As an example, the code below replaces a BaseProduct, but the old BaseProduct will not be deleted unless the new BaseProduct is created without errors:
// (code below is simplified for readability)
var baseProductToDelete = _baseProductService.Get("BP1");
var replacementBaseProduct = new BaseProduct("BP2");
using (var db = _dataService.CreateBatch())
{
db.Delete(baseProductToDelete);
db.Create(replacementBaseProduct);
// Nothing is persisted in DB until Commit finalizes the transaction.
// If an exception is thrown in Create() the entire transaction
// is rolled back.
db.Commit();
}
Use Litium.Data.DataService.CreateQuery
to query data directly from the database.
IMPORTANT - Never use
CreateQuery
on a public site without also adding a cache. When you useDataService
to retreive data you bypass the built in Litium cache.
Example:
using (var query = _dataService.CreateQuery<BaseProduct>(opt => opt.IncludeVariants()))
{
var q = query.Filter(filter => filter
.Bool(boolFilter => boolFilter
.Must(boolFilterMust => boolFilterMust
.Field("MyField", "eq", "MyValue")
.Field("MyField2", "neq", "MyValue"))
.MustNot(boolFilterMustNot => boolFilterMustNot
.Id("neq", "my_new_articlenumber"))))
.Sort(sorting => sorting
.Field("MyField3", CultureInfo.CurrentCulture, SortDirection.Descending));
int totalRecords = q.Count();
List<BaseProduct> result = q
.Skip(25)
.Take(20)
.ToList();
}
The full code-sample is available on Litium docs
The Litium .NET API contains all functionality and extension points that are available to developers. But the .NET API is only available when functionality is built as part of the .NET solution.
If you want to develop solutions as external applications and systems that can be modified and scaled without any modification or downtime of the critical e-commerce website you have to develop integrations to Litium. Web API is the most common way of connecting and Litium has made it easy to create custom secure Web APIs, Litium also has several built in Web APIs available:
The security system that is part of the built in Web APIs is also available when building custom Web APIs. It is based on Service Accounts which is a type of account that can only connect to Web API but not login to the Litium Backend.
To secure a custom API you only need to add one attribute (or both) to a controller class (or just to a single controller action):
OnlyJwtAuthorization
- Add this attribute to only allow requiests with a valid JSON Web Token (JWT)
OnlyServiceAccountAuthorization
– Add this attribute to only allow Service Accounts
to make requests
The main benefit of the Admin Web API is that it functionally covers almost all of the .NET API, but this also means that changes made in the .NET API will also change the Admin Web API.
Admin Web API reference on Litium docs
Litium Connect is a collection of APIs grouped by business domain:
(additional APIs for new business domains will be added in the future)
A major benefit of Litium connect APIs is that they have versioning that is independent of Litium platform versioning:
Litium platform | ERP API | Payments API | Shipments API |
---|---|---|---|
7.4 | 1.0 | - | - |
7.6 | 2.0 | - | - |
7.7 | 2.0 | - | - |
8.0 | 2.0 | 1.0 | 1.0 |
By looking at this version table we can see that an ERP integration built for a Litium 7.6 installation will still work when the customer upgrades to Litium 8. The integration could also be re-used for multiple installations running on different versions between 7.6-8.X.
As a general recommendation you should prefer using Litium Connect where suitable and use the Admin Web API as supplement where needed:
The source code for the Accelerator Web API used by React is available in the MVC project in folder Controllers\Api
.
Both the Admin Web API and Litium Connect allow external systems to register themselves to be notified every time a specific event occurs. These subscriptions are registered using webhooks and Litium will call all registered external endpoints when an event is triggered.
All Web APIs have Swagger API documentation that is available in every installation on URL http://domain/litium/swagger (authentication is required)
Swagger covers Accelerator Web API, Litium Connect APIs and the Admin Web API:
A Litium App is a standalone application that communicate with the Litium application using Web API.
All Payment and Shipment providers run as Litium Apps (from Litium version 8+)
The Accelerator is structured with a clear separation of business logic from presentation to make it possible to replace the MVC UI if needed.
Read more about accelerator arcitecture on Litium docs.
When Litium is installed a new Visual Studio solution is created with 5 projects (an additional e-mail project is also created). This is the Accelerator source code that can be modified in the implementation project.
The more changes you make to the codebase the bigger the project will be to upgrade to new Accelerator versions, so there is a balance when doing modifications. Changes made in the Accelerator codebase are documented in the Accelerator release notes (separated from the Litium platform release notes).
Litium.Accelerator
-projectThe Litium.Accelerator
-project is the BLL (Business logic layer) that contains all logic separated from the UI and Web API, for example:
This project has most of the Litium NuGet references.
Litium.Accelerator.Administration.Extensions
-projectContains customizations of the Litium backoffice UI, for example custom settings pages that are part of the Accelerator.
Litium.Accelerator.Elasticsearch
-projectContains the implementation of Elasticsearch, modify this code if you need to:
Litium.Accelerator.FieldTypes
-projectContains custom field types for Litiums entity field framework. The Accelerator contains one custom field called FilterField that is used to control which fields should be included in category filters.
Modify this project if you want to add your own custom field types.
Litium.Accelerator.Mvc
-projectThe web project containing stanard MVC files such as Controllers and Razor Views, but also:
/Definitions
-folder with the links between Field Templates and controllers/Client
-folder with all styles and JavaScriptRead more about the MVC project on Litium docs.
Litium.Accelerator.Email
-projectThis project can be found when looking in the solution folder on disk but is not included in the Visual Studio solution.
Client side project to manage e-mail templates (Order confirmation e-mail)
Zurb Foundation for Emails is used for styling.
Litium search is the search engine used in Litium Accelerator. It is built on Elasticsearch with additional plugins and administration interface.
Litium search runs as a separate application and a single search engine is used for all web servers in a multiserver installation.
A new Accelerator installation contains indicies for:
Additional search indicies can also be created if needed
Synonyms for Litium search can be added directly in Litium backoffice, by adding the sample below a query for trousers will also give search hits for pants, chinos and leggings.
The front-end code can be found in the Client
-folder in the MVC-project.
Foundation Zurb - Used as front-end framework
SASS - Used as CSS preprocessor
BEM (Block, Element, Modifier) - Used as naming methodology for structured CSS
Webpack - Used to build and bundle the frontend styles and scripts
Read more at https://docs.litium.com/documentation/litium-accelerators/develop/accelerator-architecture
To give the stateless Web API controllers access to the current Litium state (current page/channel/product), Litium passes that information in every request as the RequestContext
:
In the shared view \Shared\Framework\ClientContext.cshtml
the global object window.__litium.requestContext
is written to every rendered MVC-page in the Accelerator. If you try the command below in the browsers JavaScript console on a Accelerator-page
> window.__litium.requestContext
...you can see the current RequestContext
-object:
{
"channelSystemId": "9256cc22-2b93-4a4e-bfa9-04dc51ec4b05",
"currentPageSystemId": "2df58068-fe4b-40b9-851f-c49cd4f3f2bc",
"productCategorySystemId": "01015225-c6a8-4bac-ae17-834cf7617016"
}
The React http
-service is used when ajax-requests are created on the client. It serializes and attaches window.__litium.requestContext
as a header to every request:
// from \Src\Litium.Accelerator.Mvc\Client\Scripts\Services\http.js
headers: {
'litium-request-context': JSON.stringify(window.__litium.requestContext),
}
When requests are received on the server Litium looks for the litium-request-context
-header, and if the header is found its data is stored and made available in RequestModelAccessor
.
// from \Src\Litium.Accelerator.Mvc\Runtime\RequestModelActionFilter.cs
var siteSettingViewModel = request.Headers.GetSiteSettingViewModel();
Any Web API Controller (for example CheckoutController
) can then inject and use RequestModelAccessor
to get state for the request
public CheckoutController(RequestModelAccessor requestModelAccessor ...
{
...
The cart contains all the information needed to create an order.
CartContext
to modify current users cart, CartContext
is accessed by injecting CartContextAccessor
or through the extension method HttpContext.GetCartContext()
.Modify cart example:
await cartContext.AddOrUpdateItemAsync(new AddOrUpdateCartItemArgs
{
ArticleNumber = articleNumber,
Quantity = quantity,
});
Discounts are stored as OrderRows with OrderRowType set to Discount and negative price, example:
Row type | Price | |
---|---|---|
Item in cart | Product | 400 |
25% off item | Product Discount | -100 |
Order Discount | Order Discount | -150 |
Grand total | 150 |
A discount may be connected to a specific OrderRow (for example a product, fee or shipping row)
One product-row may be connected to multiple discount-rows
Discounts are always counted in a specific order:
A payment in Litium has multiple transactions that keep track of how much money is Authorized, how much of the authorized amount that is Captured or Cancelled, and how much of the Captured amount that is Refunded.
Init: Litium has initialized a payment with the Payment Service Provider (PSP)
Authorize: The buyer has committed to pay (usually this means that money is reserved in the buyers financial institution)
Capture: When money is actually moved from buyer to seller. This can only be done based on a previous authorize transaction
Cancel: An authorization can also be cancelled - can only be done based on a previous Authorize-transaction
Refund: A capture may be refunded back to the buyer - can only be done based on one or more previous Capture-transactions
Transactions also have status, a capture may for example have status pending or denied
The method TryInitializeCheckoutFlowAsync()
must be called called when the checkout is loaded, it:
TransactionType::Init
When changes are made to the cart after initialization the method CalculatePaymentsAsync()
has to be called, it:
PaymentOverview
can be used to show all Payments, Transactions and TransactionRows
A Payment Service Provider (PSP) handles the transfer of money from buyer to merchant
In Litium all PSPs run as standalone apps (hosted in Litium cloud) that use Web API to connect to the Litium platform. This lets Litium checkout page work independenly of which PSPs are installed
Litium checkout page displays the payment methods that are configured for the current Channel
Litium checkout page supports:
Hosted payment pages (like Paypal) where the buyer is redirected to PSP site
Iframe checkouts where an iframe is embedded in the checkout page, this type has two variants:
Full checkout including the collection of customer information inside the iframe (like Klarna/Svea)
Collecting payment information only inside the iframe (like Adyen), then Litium collects the customer information separately
The SalesOrder is created and saved to the database when a PSP App notifies Litium that money is reserved for a payment
The SalesOrder contains items (order rows) and the information required to fulfill the order:
A payment Authorize-transaction is created, it has a reference to the previous Init-transaction that was created during payment intialization
Use OrderOverviewService
to get the OrderOverview
to access all payments, shipments and returns connected to a SalesOrder
.
When an order is placed it needs to be fulfilled (shipped).
This process is normally done through integration to an ERP system:
Litium sends a placed order to ERP to start the fulfilment process
ERP notifies Litium when a shipment is ready to send
Litium creates a new Shipment in Init state and calculates the value of the shipment
The shipment state is changed to Processing, in the shipment state transition event the payment is captured:
A payment Capture-transaction is created in Litium
Money is captured through the PSP App
PSP notifies Litium through a callback that the payment is captured
Litium sets the shipment to state ReadyToShip and the ERP is notified
ERP notifies Litium when the delivery has been handed over to a delivery provider (e.g. DHL)
Litium sets shipment state to Shipped
Read more
Init is set when the shipment is created
Setting a shipment to Processing will trigger payment capture
ReadyToShip is set when all payments for a shipment are captured
Shipped is set when the shipment is handed over to the shipping company (not when delivered to buyer)
A shipment can also go from Processing to Cancelled
The order is set to Confirmed when at least one payment is guaranteed (has an Authorize-transaction)
An order can be only partially fulfilled but still completed, it is set to Completed when
All Shipments for the order are resolved (Shipped or Cancelled)
All Payments for non cancelled Shipments are resolved (Captured or Cancelled)
Add StateTransitionValidationRules
to make sure that Orders/Shipments meet the requirements needed to change state.
public class ProcessingToCompletedCondition : StateTransitionValidationRule<SalesOrder>
{
// (non relevant code has been removed)
public override string FromState => Sales.OrderState.Processing;
public override string ToState => Sales.OrderState.Completed;
public override ValidationResult Validate(SalesOrder entity)
{
var result = new ValidationResult();
var order = _orderOverviewService.Get(entity.SystemId);
if(!HasAllShipmentsShipped(order))
{
result.AddError("Shipment", "All shipments are not shipped.");
}
return result;
}
}
Add event listeners to act when Orders/Shipments change state.
// (This code can be found in the tAccelerator)
[Autostart]
public class SalesOrderEventListener : IAsyncAutostart
{
// (non relevant code removed)
ValueTask IAsyncAutostart.StartAsync(CancellationToken cancellationToken)
{
_eventBroker.Subscribe<SalesOrderConfirmed>(x => _stockService.ReduceStock(x.Item));
_eventBroker.Subscribe<SalesOrderConfirmed>(x => _mailService.SendEmail(/* params */);
return ValueTask.CompletedTask;
}
}
A tag is a string key added to an order, shipment or payment
Events are raised when tags are added or removed
Tags can for example be used to set values that can be checked in state transition validations
Tags are used in the Accelerator to handle the B2B order approval flow by setting a "Waiting for approval"-tag on orders
Litium requires a Microsoft SQL Server 2019 database.
The command-line donet tool litium-db is used to manage the database, it can be used to:
All Litium custumers have independent installations and many of these are not yet upgraded to the latest version 8. A short background is useful to better understand the architecture when maintaining older installations.
The current Litium version 8 has a SPA-backoffice and a modern, testable, service based architecture built using the latest version of .NET. Litium version 4 had an outdated architecture, dependencies on .NET Framework and needed a full rewrite, but instead of rewriting the entire platform at once this was done in steps.
So depending on which version of Litium you work with you will find different architectures in the different areas depending on which has been rewritten in that version.
You can find information about features currently in development in the roadmap:
https://docs.litium.com/documentation/roadmap
A Litium Solution is upgraded in three steps:
The Litium platform is upgraded using NuGet update to upgrade all packages to the latest version
The database is upgraded using the Litium db-tool
Use a connectionstring to upgrade instantly or generate a script to upgrade manually
There is no "downgrade" so make sure you have a saved backup before running the upgrade
Upgrading Litium Accelerator is manual
This is likely a big task depending on how big the gap is between the old and new version
The latest released Accelerator can be used as reference but the task to merge the modified solution with the new Accelerator sourcecode is still likely a big task