Multi-pass Calculation

Use Case

When a price of a product “B” depends on (i.e. requires it for its own calculation) a price of another product “A” within the same price list (or LPG, Quote), then you need to calculate first the price of product “A” and only then the price of “B”.

Process Flow

From a technical perspective there is no guarantee that line items are calculated in any specific order, so you can have two situations:

  • The product “A” was calculated first (and so its price is already stored in a DB) and the “B” product is calculated afterwards, so the system can already retrieve the price of “A”, because it is available, and it can calculate the price of “B” (which depends on the price of “A”).

  • Or the “B” product comes to the calculation first, when “A” does not have the price yet. In this case the calculation of the “B” product must wait – i.e. the logic must schedule the line item for re-run (“mark as dirty”). Once the system finishes processing of all line items, it will verify if some lines were marked as dirty and if so, it starts the next pass (next round) calculation for all of the lines marked as dirty.
    In addition to marking the current line item as dirty from line-item logic, header logic can also mark specific line items (or entire folders) as dirty during the Recalculate Changes pre-phase, using the markItemDirty(lineId) method. This is useful when several line items are logically linked (for example, a main product, shipping, and accessories) and must be recalculated together even if only one of them was directly modified.

Notes:

  • You can do as many passes as needed.

  • The product without any dependency is sometimes referred to as “master” or ”base” product.

  • In case of distributed/parallel calculations:

    • It can happen that “A” and “B” will be calculated on different nodes. Nevertheless, the same rules apply – if the logic starts calculation of the dependent product “B” and the price of the master product “A” is not yet available, the logic will mark the line as dirty, and once the first pass finishes on all nodes, then the system will start the second pass for all “dirty lines” (possibly again distributed across multiple nodes).

  • Failure in a dirty pass calculation will leave the item dirty. In case of permanent failure, the dirty pass is recalculated until the maximum number of passes is reached. Sometimes, it is preferable not to retry but to mark the item as clean in case of a calculation error. This special behavior is optional and can be configured on a per-instance basis (on a cluster level), defaulting to the current behavior. Set the calculationTasks.alwaysCleanItems (<alwaysCleanItems>false</alwaysCleanItems>) to true to mark all items as clean upon a calculation failure – then the following dirty runs will not start.

  • Keep in mind that keys within the map being cached are converted to String in the multi-pass calculation (for example, when using the libs.SharedLib.CacheUtils.getOrSet to store the data in the cache).

Process Sample

image-20200821-142139.png

Technical Implementation

Conceptual Code Sample

Groovy
def price

if (productDoesNotDependOnAnother) {
    /* price of the product does not depend on another product */
    /* so we can calculate the price right away */
    price = calculatePrice()

} else {

    /* price of the product is derived from price of other/master product */
    if (api.getIterationNumber() == 0) {
        /* if this is first pass, then we need to wait for the related product to be calculated */
        api.markItemDirty()

    } else {

        /* this is already second pass, so we expect the other/master product to be calculated already */
        masterProductPrice = api.currentContext(skuOfTheMasterProduct)?.Price
        price = calculatePriceFrom(masterProductPrice)
    }
}

return price

Header Logic Example (Recalculate Changes Pre-Phase)

Groovy
// Header logic – pre-phase during "Recalculate Changes"
def relatedLineIds = findRelatedLineItems(mainLineId)

for (lineId in relatedLineIds) {
    // Ensure that logically linked items are recalculated together
    markItemDirty(lineId)   // marks the given line dirty for the Recalculate Changes run
}

The important functions here are:

  • api.getIterationNumber() – Returns the current pass number (starting from 0 for the first pass), so the logic can distinguish between the initial run and subsequent passes.

  • api.markItemDirty() – Marks the current line item as dirty so that it will be recalculated in the next pass.

  • markItemDirty(lineId) – Available in header / pre-phase logic and similar contexts. Marks the specified line item as dirty so that it is included in the Recalculate Changes processing even if the user did not change it directly. When called for a folder line item, all nested child line items are marked dirty as well. (Available from version 17.0)

  • api.currentContext(sku) – Allows you to access the calculated result (for example, Price) of another product within the same calculation context.

References