In this blog post I would like to present you an implementation for an issue we all might have suffered from. But let us start in the beginning and shortly discuss how pricing works in Sitecore XC in general.
How does pricing work in Sitecore XC?
The most basic way to set a price in Sitecore XC is, that we just set a price on the sellable item directly as List Price. This price can be of course set per currency like seen in the example below.
Because such a price setting is quiet inflexible Sitecore has build some piece of logic on top of that. The core principle of that mechanism is so define a price based on currency, date, and quantity, which is far more flexible, than just set one single price per sellable item.
This can be edited by an editor under the Pricing Application in the Business Tools of Sitecore XC.
Once you entered this application you are able to define a so called Pricebook.
Such a Pricebook can be linked to one or more catalogs and is itself just a container for more specific price settings, the so called Pricecards.
Such a Pricecard then be directly applied on a sellable item to a sellable item via the Pricing field.
Now the interesting part starts, where real price settings come into play. Pricecards themselves are again just containers for so called Snapshots.
A snapshot itself then contains the information of a start date, when this specific snapshot should be applied, a tiered price per currency, which means you can define how much one single sellable item costs, in case a customer has X items in cart. And last but not least, you are able to tag the snapshot to define to which sellable items this specific snapshot should be applied.
Therefore you see, that this pricing mechanism is far more flexible and powerful, than just adding a simple list price to a sellable item directly.
Known limitations of snapshots
This overall pricing mechanism is really great and powerful. But when talking to clients and collecting all their wishes and requirements, one feature was wished very often, which is currently not possible to fulfill with the OOTB capabilities. The client always asks exactly the following question
How can I set an end date for the snapshots?
Right now the behavior of snapshots is very rough described like this:
- Once a Pricecard is linked correctly to a product, Sitecore grabs all the snapshots and checks if the start dates of the snapshots are greater than the current date time.
- Then Sitecore sorts all the snapshots descending
- Finally Sitecore just grabs the first element from that list and evaluates its pricing information
This means, than if the start date of multiple snapshots have been reached and therefore multiple snapshots would be valid, Sitecore takes the latest one to ensure, that at a specific time just one single snapshot is valid.
So for a content author this means, he has to create a snapshot to determine a specific pricing. Once he wants to change that pricing for a specific period of time, e.g. to offer a special price on a special day or a special time span, he would have to create a new snapshot with the new pricing information and and the proper start date. So now the issue starts. Because the author cannot set a specific end date, this price would be valid “forever”. And because we do not want to have that price forever, the author now has to create another snapshot with the old pricing again and the proper start date. This start date has then to be the date, when the special pricing should end. So instead of just creating two snapshots with proper start and end date, the author has to workaround that and create a third helper snapshot to restore old pricing again.
Of course this works, but to be honest is quiet unwieldy. We also asked Sitecore about the possiblity to set an end date on snapshots. The answer was, that they know about that missing feature, and that they are working on implementing that for a later version. But when exactly that feature is implemented by Sitecore themselves is not officially known.
The recommendation of Sitecore then was to implement such a feature on our own.
How to get rid of this limitation?
The solution is as simple as it might sound. We just implement the possibility to set and end date on snapshots, save that properly and then extend the snapshot evaluation while retrieving pricing information. Sounds easy, right? And yes it is, if you know where to hook in. Let’s have a rough overview of all todos:
- Extend the form to enter an end date in addition to the start date
- Extend the snapshot to also hold information about end date
- Extend the action, which saves the start date to the snapshot to also save the end date to the snapshot
- Extend the view to display also the end date in addition to the start date
- Extend sellable item and variation sell price calculation to respect the new end date field
Let us start with the task to extend to input form, where an editor can specify the Begin Date. Of course now we would like the user also to be able to specify an End Date the same way.
How do we achieve that?
Quiet simple! I knew this piece of code I need should be in the Pricing Plugin provided by Sitecore. Therefore I simply reflectored that dll to find something close to edit / show snapshot details. After searching a while, I also found the right class GetPriceSnapshotDetailsViewBlock, which you can see below.
So I just took that piece of code and added the new field End Date in the same way Start Date was added. You will see, that I slightly modified the OOTB code a bit also for the standard fields. The reasons for that were, that I wanted to change the date time format a bit when displaying the fields and that I wanted to include another field just in edit mode. But more details later on.
After adding all the stuff properly the new output of the dialog was the following.
So next step is of course, that once an editor sets a proper End Date, we also want this date to be stored somehow on the Snapshot.
To do this, we have to implement a custom DoAction block, which is responsible for taking the properties entered in edit mode and saving them to the entities or components. Again I crawled in the dll and found the proper class DoActionAddPriceSnapshotBlock.
So again, I created my own version of that class and added piece of logic to also extract the End Date. The only problem, which now came up was, the issue, how the values were stored on the price card snapshot. Sitecore used a custom command for that. Unfortunately the parameter of that command did not really fit to the new situation of having now also an end date. As you can see, it just accepted a begin date. Of course there were also other overloads of that call, but more or less, they did not fit as well or would lead to a long rat tail I would have to handle. Therefore I decided to just create my own command, based on the given one, and extend also this piece of code to handle End Date properly. The basic implementation can be seen below.
My custom implementation is really nothing more, than a command with another endpoint also accepting the parameter End Date and a piece of logic to include the End Date into the snapshot.
Because we are in a an environment, where we prefer composition over inheritance, I had to create a new component, which in the end holds the End Date property.
If Sitecore implements that feature on their own, I am pretty sure, they will just add another property directly in the Snapshot component, like it was done with Start Date. But in the end it does not matter if the End Date property is located directly in the Snapshot component or in another child component. You just have to know where it is for later usage. But that’s already all the magic to set the End Date Property from the point the editor enters it to the point it is part of the Snapshot. Below you can now the the result, after saving.
Note: Here you can already see, that I changed the output date format also a bit to also display time and not only date, which makes it much easier for editors to set Start and End Date minute-by-minute.
Now let’s come to the more interesting part of the implementation. After we have successfully set the End Date we of course also have to evaluate it properly. To do that, I had to find out, where the Snapshots are evaluated at all to also hook in there.
After some time crawling through the dll, I found exactly, what I needed. CalculateSellableItemSellPriceBlock for sellable items sell price calculation and CalculateVariationsSellPriceBlock for variants of sellable items sell price calculation. Within these classes you can see, that the calculation included functions like ResolveSnapshotByCard and ResolveSnapshotByTags, which in the end use functions like FilterPriceSnapshotsByDate and FilterPriceSnapshotsByTags.
In the screenshots above I highlighted exact the location in code, where the snapshot is evaluated. The only thing now we have to do is, that we have to also include the new End Date in that calculation. For demonstration reason I chose the approach to create new classes, one for sellable item sell price calculation and one for variations, the inherited from the existing once and just overwrite the virtual functions to include our new evaluation.
In the End the functions in the new class look like this.
Just a simple evaluation if the End Date has not been reached.
Note: You can see here already an extension I made for End Date evaluation, which I describe in the end in more Detail
Now let’s see all that in action. I created a Pricecard called Demo and attached it to my demo product.
Then I created two snapshots on it. One snapshot should be valid from now to some date far in the future (or has no End Date). And one snapshot should only be valid for just a small time span, in this case just a few minutes. After that, the price of the snapshot before, should automatically take over again, without the need to create a third snapshot. The two snapshots can be seen below.
Now let’s put that sellable item into the cart and see what happens over the time.
We start with a time after the start date of the “normal” price and before the start date of the “special” price.
The result is exactly, what we wanted to have. Because the first snapshot is valid and the second not, Sitecore uses the price from the first snapshot to calculate sell price.
Now let’s call the cart a bit later. The time now falls into the start date of the first AND the second snapshot and also is located within the first and second snapshot end date. From Sitecore OOTB logic, it now should take the snapshot, which start date is closer to the current date time.
And this is also the case, like seen in the screen above. Sitecore uses now our special price instead of the normal price, when we call the cart again.
Last but not least we now want to test the really interesting case. What happens now if we call the cart at a time, where both snapshot start dates are matching, but the end date has expired. We would expect now, that because the end date of the special price snapshot has expired, Sitecore automatically chooses the normal price snapshot again according to the new implementation.
I know the sceeen above looks a bit wild, but displays exactly what happens. When we call the cart again at a time after the end time of the special price, Sitecore really takes the normal price again. As you can see the start and end date of the normal price match. And the start date of the special price matches. But because the end date, displayed in red, does not match, Sitecore does not use that snapshot, like it would do in standard implementation, but uses the first snapshot and uses its price as a valid sell price.
Note: Of course we could come into the situation, where no snapshot is valid, because all the end dates would be reached. But that is also no problem. In such scenarios, Sitecore’s own fallback takes over and uses as fallback to the price card logic simply the list price directly set on sellable item. But of course, if this price would not exist you are screwed and the sellable item would not be sellable anymore and would lead to an error, if trying to add to cart.
Now let me finally show you one little extension I already made, while implementing End Date handling. I believe, that there are scenarios, where some basic price really should have just a start date and no end date. Because the OOTB Date Time field always needs a value to be set, I worked around that and created in addition a small checkbox to let the editor decide, whether the really wants to set an end date. In case he want, he just checks the checkbox and everything is like described above.
In case he does not check the checkbox, the DoAction block I created just ignores, what was transmitted in End Date field and simply writes DatetimeOffset.MinValue into the End Date property. This then can later on be checked and treated e.g. like an non existing value in display, like seen below.
And as seen before in the sell price calculation we can also react on that specific value to avoid End Date evaluation. This way, in case you do not set an End Date for a snapshot it acts exactly as it did OOTB before.
Within this blog post I showed you easy it can be in general to implemented some missing feature in Sitecore XC. I showed how to start, where to look any how to hook in correctly at the specific locations, which need to be changed.
Within this special change we created a feature, which allows the editor to set additionally to the start date also an end date for price card snapshots to be even more flexible in creating complex pricing scenarios.
As always the full demo code can be found in a repository on my github and as always this code is not production ready and should be used used as an entry point or for demonstration reason.