Skip to main content

Shopping cart/order calculation

Version: 5.6.0

Shopping cart/order calculation

Concept

What Prompted the Redesign:

  • An awareness that the existing system had reached a point where accumulated changes were starting to impact maintainability and clarity.
  • The need to implement split shipments — specifically, the requirement to calculate shipping rates for each shipment.
  • Adoption of an API‑First approach.

Core Principles of the New Design:

  • Maximize isolation of the order calculation logic from the rest of the codebase.
  • Structure the calculation process so that it is easy to interact with and extend.

Diagram

cart-calculation.svg

LogicCalculationAction

XCart\Logic\Action\Order\CalculateOne\Action

This is the outer layer of the calculation process.

Parameter - \XCart\Logic\Action\Order\CalculateOne\DTO\Request

Return value (\XCart\Bundle\ApiCoreBundle\Logic\DTO\Response\ResponseInterface)

  • \XCart\Logic\Action\Order\CalculateOne\DTO\Response - Returned in case of success
  • \XCart\Bundle\ApiCoreBundle\Logic\DTO\Response\EmptyFailedResponse - Returned in case of an error, if the DomainCalculationAction returns an instance of \XCart\Bundle\ApiCoreBundle\Logic\DTO\Response\FailedResponseInterface

Essentially, this component receives an order object (XLite\Model\*) as input and returns a calculated order object (XLite\Model\*) or an empty response (EmptyFailedResponse) as output.

DomainRequestAssembler

\XCart\Logic\Action\Order\CalculateOne\Assembler\DomainRequestAssembler

Converts \XCart\Logic\Action\Order\CalculateOne\DTO\Request { currently composed of separate parts (Order, Context, Mode → Request), but this can be refactored to (Request → Request) } into \XCart\Domain\Action\Order\Calculate\DTO\Request .

DomainCalculationAction

\XCart\Domain\Action\Order\Calculate\Action

Contains the core logic for order calculation (see further details below).

order.after_calculation event

The order.after_calculation event is dispatched via the eventDispatcher. It is used to trigger a side effect — for example, adding a TopMessage

Enrich

All enrichers (services tagged withapp.logic.order.calculate.enricher and implementing the interface \XCart\Logic\Action\Order\CalculateOne\Enricher\OrderEnricherInterface) are executed.

Ensures that data from DomainResponse (DomainCalculationAction) is transferred into the Order (XLite\Model\*)

Is executed only if DomainCalculationAction returns a successful result.

Additionally, CartCalculatedVersion is synchronized with CartVersion, and CartChangesDiff is reset to zero.

DomainCalculationAction

\XCart\Domain\Action\Order\Calculate\Action

AssembleContext

\XCart\Domain\Action\Order\Calculate\Assembler\ContextAssembler

Generates Context for Calculators from Request

NormalizeItems

\XCart\Domain\Action\Order\Calculate\Normalizer\ItemsNormalizer

The exact purpose of this step is currently unclear.

SurchargesOverride

In regular calculation mode, surcharges from the context are forcibly added to the order and its items. This allows overriding values for AOM.

PreCalculate

\XCart\Domain\Action\Order\Calculate\Calculator\PreCalculator

All pre-calculators (services tagged with app.order.calculate.pre_calculator and implementing the interface \XCart\Domain\Action\Order\Calculate\Calculator\PreCalculatorInterface) are executed.

Calculator

CalculateShipments

\XCart\Domain\Action\Order\Calculate\Manager\OrderShipmentManager

calculate-shipments.svg

OrderItemStockCalculator

\XCart\Domain\Action\Order\Calculate\Calculator\Shipment\OrderItemStockCalculator

In the OrderItems, inventory data is entered broken down by warehouse.

ShipmentsAssembler

\XCart\Domain\Action\Order\Calculate\Assembler\ShipmentsAssembler

All assemblers (services tagged with app.order.calculate.shipments_assembler and implementing the interface \XCart\Domain\Action\Order\Calculate\Assembler\ShipmentsAssemblerInterface) are executed.

The logic for splitting shipments is divided across four assemblers in the core (\XCart\Domain\Action\Order\Calculate\Assembler\*):

  • InitialShipmentsAssembler (priority: 1000)

    Products are split into two groups:

    • Items available for shipping based on current inventory (in_stock)
    • Items unavailable for shipping (unavailable)
  • DefaultShipmentsAssembler (priority: 900)

    The in_stock group is further split into separate shipments using allocators (services tagged with app.order.calculate.shipments_assembler.allocator and implementing the interface \XCart\Domain\Action\Order\Calculate\Assembler\ShipmentsAllocator\ShipmentsAllocatorInterface)

    Only the first allocator that returns true in itsisAvailable method will be used.

  • UnavailableShipmentsAssembler (priority: 150)

  • FinalShipmentsAssembler (priority: 100)

Calculation Groups

The main calculation process runs through multiple iterations for each group. For each group, all calculators are triggered; however, in the implementation it is possible to determine the current group to determine whether to execute.

Groups are defined in assemblers (services tagged withapp.order.calculate.calculate_group_assembler and implementing the interface \XCart\Domain\Action\Order\Calculate\Assembler\CalculateGroupAssemblerInterface)

GroupPreCalculator

All pre-calculators (services tagged with app.order.calculate.group_pre_calculator and implementing the interface \XCart\Domain\Action\Order\Calculate\Calculator\GroupPreCalculatorInterface) are executed.

Within the calculators, the order object (XCart\Domain\Entity\Order) is enriched with additional data, which will later be used to generate surcharges (Surcharges).

ProcessSurcharges

For each individual item in the order, services tagged with app.order.calculate.item_surcharge_modifier and implementing the interface \XCart\Domain\Action\Order\Calculate\SurchargeModifier\ItemSurchargeModifierInterface are executed. To determine which calculation group the modifier should run in, the service must implement the method isSupportGroup. These modifiers are intended to add surcharges to specific order items.

Next, services tagged with app.order.calculate.item_surcharge_modifier are executed. These must implement the interface \XCart\Domain\Action\Order\Calculate\SurchargeModifier\SurchargeModifierInterface . The method isSupportGroup must be implemented to define the appropriate calculation group for modifier execution. These modifiers are intended to add surcharges at the order level.

GroupPostCalculator

Post-calculators (services tagged with app.order.calculate.group_post_calculator implementing the interface \XCart\Domain\Action\Order\Calculate\Calculator\GroupPostCalculatorInterface) are executed.

There are no existing implementations, so the purpose of this stage is unclear.

PostCalculator

\XCart\Domain\Action\Order\Calculate\Calculator\PostCalculator

Post-calculators (services tagged with app.order.calculate.post_calculator implementing the interface \XCart\Domain\Action\Order\Calculate\Calculator\PostCalculatorInterface ) are executed.

There are no existing implementations, so the purpose of this stage is unclear.

TotalCalculator

\XCart\Domain\Action\Order\Calculate\Calculator\TotalCalculator

Total-calculators (services tagged with app.order.calculate.total_calculator implementing the interface \XCart\Domain\Action\Order\Calculate\Calculator\TotalCalculatorInterface ) are executed.

Integration (Extending via modules)

Adding a Custom Surcharge with Custom Logic

To add a custom surcharge, you need to implement it via the SurchargeModifier service.

You must create a class that implements the \XCart\Domain\Action\Order\Calculate\SurchargeModifier\SurchargeModifierInterface interface and ensure that this class is registered in the DI container with autoconfiguration enabled.

In the calculate method of your class, you should add an instance of XCart\Domain\Entity\Order\Surcharge with the necessary data to the list of surcharges (the first method parameter).

In some cases, part of the logic can be moved to a separate GroupPreCalculator service. This allows other modules to more easily influence the surcharge value.