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
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
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
)
- Items available for shipping based on current inventory (
DefaultShipmentsAssembler (priority: 900)
The
in_stock
group is further split into separate shipments using allocators (services tagged withapp.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.