- Get Started
- Functionality
- Learning
- Extension
- Dev Tooling
- APIs
- Releases
Author:
Holger Lierse
Changed on:
7 July 2026
`util-sourcing`), designed to simplify implementing complex sourcing logic and reduce repetitive code.PrerequisitesBefore diving in, make sure you have:Author:
Holger Lierse
Changed on:
7 July 2026
`util-sourcing` library is a comprehensive collection of utility functions designed to minimize the overhead and complexity of writing sourcing logic in your Fluent Commerce rules.PrerequisitesThese articles assumes you're familiar with:`SourcingUtils`): Orchestrates the sourcing process and provides helper methods to load a Sourcing Profile`SourcingContextUtils`): Loads and manages Sourcing Context `OrderUtils`): Performs order-specific operations such as fulfillment creation`LocationUtils`): Provides location-based helpers including distance calculations and caching`FulfilmentOptionsUtils`): Creates fulfillment plans with available quantity tracking and ETA calculationAuthors:
Holger Lierse, Kirill Gaiduk
Changed on:
7 Aug 2026
`SourcingUtils` class in the `util-sourcing` bundle is the main utility class that orchestrates the entire sourcing process within a rule. It provides core helper methods such as loading a sourcing profile, initializing the sourcing context, and executing the sourcing logic configured in the profile. Each stage supports multiple customization points.`getPlanFinder` provides a single entry point for primary and fallback sourcing, generating multiple ranked plans without reloading data`getPlanFinder()``SourcingPlanFinder` that exposes:`nextFullPlan()` for primary-strategy sourcing `nextFallbackPlan()` for fallback-strategy sourcing`findPlanBasedOnStrategies()``ChunkLoadingPlanFinder` that loads locations in incremental chunks sized proportionally to the order's item count. `getBasedOnStrategiesPlanFinder()``SourcingPlanFinder` configured for primary sourcing strategies.`findPlanBasedOnStrategies(SourcingExecutionParameters)` is equivalent to `getBasedOnStrategiesPlanFinder(sourcingExecutionParameters).nextPlan()`.Use this when you need to call `findPlanForAllItems` multiple times without reloading sourcing data from scratch - for example, when generating a ranked list of fulfillment plans.`findPlanBasedOnFallbackStrategies()``getBasedOnFallbackStrategiesPlanFinder()``SourcingPlanFinder` configured for fallback sourcing strategies.`findPlanBasedOnFallbackStrategies(SourcingExecutionParameters)` is equivalent to `getBasedOnFallbackStrategiesPlanFinder(sourcingExecutionParameters).nextPlan()`.Wrap the returned finder (for example, for multi-attempt location exclusion) before calling `findPartialFulfilmentPlan` in a loop.`buildRejectedFulfilment()``findPlanForAllItems()``findPlanBasedOnStrategies` method to identify a plan for an order based on the sourcing strategies. It ranks candidate locations using the provided sourcing criteria, and searches for the best combination of locations that can cover the full order within the allowed split limit. Fewer-location plans are always preferred.Before entering permutation search, an early availability check sums available quantities for each required item across all candidate locations. If any item cannot be covered, permutation search is skipped. If no valid combination exists, it returns an empty plan.`findHighestValuePartialFulfilment()``findPlanBasedOnFallbackStrategies` method to find the highest-value partial fulfillment. It filters out excluded locations, compares each candidate's rating based on the remaining unfulfilled items at that iteration, and checks whether the location can cover at least part of the remaining items. The method returns the best fulfillment found or none if no positive-value option exists.`findPartialFulfilmentPlan()``findHighestValuePartialFulfilment` until the `maxSplit` limit is reached or all unfulfilled items are covered. Location ratings are recalculated after each allocation iteration.Use `getBasedOnFallbackStrategiesPlanFinder` to obtain a pre-loaded `SourcingPlanFinder`, then wrap it (for example, for multi-attempt location exclusion) before calling `findPartialFulfilmentPlan` in a loop.`loadPositions()``findPlanForAllItems` and `findPlanBasedOnFallbackStrategies`) to load inventory.`loadSourcingProfile()``getUnfulfilledItems()``getNetworkRef()``getVirtualCatalogueRef()``getMaxSplit()`Author:
Holger Lierse
Changed on:
17 Sept 2025
`SourcingContextUtils` in the `util-sourcing` is a utility class for managing sourcing context and data loading operations. It provides methods to create and populate sourcing contexts with order details, unfulfilled items, and supporting data required for sourcing decisions.`loadSourcingContext()`Authors:
Holger Lierse, Kirill Gaiduk
Changed on:
13 Aug 2026
`OrderUtils` class in the `util-sourcing` is a utility class that provides order-specific utilities for sourcing operations. It handles order-related sourcing operations including fulfillment creation, fulfillment type determination, and order item management.`createFulfilments()``SourcingPlan`, allocating order items to locations and assigning fulfillment types. `FulfilmentItem` includes an `availableQty` field that records the available quantity at the sourcing location at the time of plan generation`refs` are assigned deterministically (`{orderId}-{fulfilmentChoiceId}-{index}`) - derived from the `SourcingContext` Id (e.g., order Id), fulfillment choice Id, and a consecutive index starting from the number of already existing fulfillments. This ensures that two sourcing processes running concurrently for the same fulfillments produce identical refs, preventing duplicate fulfillment creation`fillFulfilmentType()` `itemsMinusFulfilments()`Author:
Holger Lierse
Changed on:
17 Sept 2025
`LocationUtils` class in the `util-sourcing` is a utility class that provides utilities for location-based sourcing decisions. It handles location-specific sourcing logic including distance calculations, location availability checks, and provide location-based caching optimization.`getLocationByRef()``getLocationsInNetwork()` `getLocationsInNetworks()``distanceInMetres()`Author:
Kirill Gaiduk
Changed on:
9 July 2026
`FulfilmentOptionsUtils` class in the `util-sourcing` library creates `FulfilmentPlan` records for Fulfillment Options from a `SourcingPlan`. It captures the proposed fulfillment options, available quantities, and optional ETA values, helping rules present calculated sourcing outcomes without recreating fulfillment-plan mapping logic.`SourcingPlan` as a `FulfilmentPlan` under the fulfillment option entity`EtaCalculator` to calculate and set ETA on the plan and on each individual fulfillment. When no `EtaCalculator` is provided, ETA fields are omitted`FulfilmentItem` in the plan records the available quantity at the sourcing location at the time of plan generation`createFulfilmentPlan()``SourcingPlan` as a `FulfilmentPlan` under the fulfillment option entity.`context`, `plan`, or `sourcingContext` is `null`, or if the plan contains no fulfillments`productRef`, `requestedQuantity`, and `availableQuantity``EtaCalculator` is provided, ETA is calculated and set for the plan and for each fulfillment separately1// Load the sourcing profile for the current context
2SourcingProfile sourcingProfile = SourcingUtils.loadSourcingProfile(context);1// Create sourcing context with unfulfilled order items
2SourcingContext sourcingContext = SourcingContextUtils.loadSourcingContext(
3 context,
4 SourcingUtils::getUnfulfilledItems
5);1// Find the best sourcing plan based on strategies
2SourcingPlan plan = SourcingUtils.findPlanBasedOnStrategies(
3 context,
4 sourcingContext,
5 sourcingProfile,
6 ImmutableList.of(Constants.Status.ACTIVE), // Consider only ACTIVE positions
7 this::customInventoryProcessor
8);1// The system generates a sourcing plan that might look like:
2SourcingPlan optimalPlan = new SourcingPlan();
3optimalPlan.addFulfilment(
4 SourcingUtils.Fulfilment.builder()
5 .location(Location.builder().name("Manhattan Store").build())
6 .items(Arrays.asList(
7 Fulfilment.FulfilmentItem.builder().ref("2x Laptops").build(),
8 Fulfilment.FulfilmentItem.builder().ref("3x Mice").build())
9 )
10 .build()
11);
12optimalPlan.addFulfilment(
13 SourcingUtils.Fulfilment.builder()
14 .location(Location.builder().name("NJ Warehouse").build())
15 .items(Arrays.asList(
16 Fulfilment.FulfilmentItem.builder().ref("3x Mice").build())
17 )
18 .build()
19);1// Set fulfillment types
2OrderUtils.fillFulfilmentType(sourcingContext, fulfilments);
3
4// Create fulfillments from the plan
5List<Fulfilment> fulfilments = OrderUtils.createFulfilments(
6 context,
7 sourcingContext,
8 plan.getFulfilments()
9);1SourcingExecutionParameters params = SourcingExecutionParameters.builder()
2 .context(context)
3 .sourcingContext(sourcingContext)
4 .profile(sourcingProfile)
5 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
6 .build();
7
8SourcingPlanFinder finder = SourcingUtils.getPlanFinder(params);
9
10// Primary sourcing: call multiple times to produce alternative
11// plans without reloading sourcing data
12SourcingPlan plan = finder.nextFullPlan();
13
14// Fallback sourcing from the same parameters object
15SourcingPlan fallbackPlan = finder.nextFallbackPlan();1SourcingPlan plan = SourcingUtils.findPlanBasedOnStrategies(
2 SourcingExecutionParameters.builder()
3 .context(context)
4 .sourcingContext(sourcingContext)
5 .profile(profile)
6 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
7 .build()
8);1SourcingPlanFinder planFinder = SourcingUtils.getBasedOnStrategiesPlanFinder(
2 SourcingExecutionParameters.builder()
3 .context(context)
4 .sourcingContext(sourcingContext)
5 .profile(profile)
6 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
7 .build()
8);
9// Call nextPlan() for each attempt without reloading sourcing data
10SourcingPlan plan = planFinder.nextPlan();1SourcingPlan plan = SourcingUtils.findPlanBasedOnFallbackStrategies(
2 SourcingExecutionParameters.builder()
3 .context(context)
4 .sourcingContext(sourcingContext)
5 .profile(profile)
6 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
7 .build()
8);1SourcingPlanFinder fallbackFinder = SourcingUtils.getBasedOnFallbackStrategiesPlanFinder(
2 SourcingExecutionParameters.builder()
3 .context(context)
4 .sourcingContext(sourcingContext)
5 .profile(profile)
6 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
7 .build()
8);
9// Wrap finder for multi-attempt location exclusion, then call nextPlan()
10SourcingPlan partialPlan = fallbackFinder.nextPlan();1// Build system rejected fulfillment for unfulfillable items
2Fulfilment rejectedFulfilment = SourcingUtils.buildRejectedFulfilment(
3 context,
4 sourcingContext,
5 context.getProp(PROP_SYSTEM_REJECTED_LOC_REF)
6);1String networkRef = SourcingUtils.getNetworkRef(sourcingProfile, strategy);
2String virtualCatalogueRef = SourcingUtils.getVirtualCatalogueRef(sourcingProfile, strategy);
3Integer maxSplit = SourcingUtils.getMaxSplit(sourcingProfile, strategy);
4List<Location> locations = LocationUtils.getLocationsInNetwork(context, networkRef);
5
6SourcingPlan plan = SourcingUtils.findPlanForAllItems(
7 SourcingExecutionParameters.builder()
8 .context(context)
9 .sourcingContext(sourcingContext)
10 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
11 .build(),
12 virtualCatalogueRef,
13 locations,
14 sourcingCriteria,
15 maxSplit
16);1Set<String> excludedLocations = Set.of("warehouse-001", "store-002");
2Optional<Fulfilment> partialFulfilmentOpt = SourcingUtils.findHighestValuePartialFulfilment(
3 locationAndPositions,
4 unfulfilledItems,
5 excludedLocations
6);
7
8if (partialFulfilmentOpt.isPresent()) {
9 Fulfilment partialFulfilment = partialFulfilmentOpt.get();
10
11 // Calculate remaining items after partial fulfillment
12 List<OrderItem> remainingItems = OrderUtils.itemsMinusFulfilments(
13 unfulfilledItems,
14 Arrays.asList(partialFulfilment)
15 );
16
17 // Create the partial fulfillment
18 OrderUtils.fillFulfilmentType(sourcingContext, Arrays.asList(partialFulfilment));
19 OrderUtils.createFulfilments(context, sourcingContext, Arrays.asList(partialFulfilment));
20
21 context.action().log("Created partial fulfillment with {} items",
22 partialFulfilment.getItems().size());
23}1SourcingPlanFinder fallbackFinder = SourcingUtils.getBasedOnFallbackStrategiesPlanFinder(
2 SourcingExecutionParameters.builder()
3 .context(context)
4 .sourcingContext(sourcingContext)
5 .profile(profile)
6 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
7 .build()
8);
9SourcingPlan partialPlan = SourcingUtils.findPartialFulfilmentPlan(
10 sourcingContext,
11 fallbackStrategy,
12 unfulfilledItems,
13 locationsWithPositions,
14 criteria,
15 locationLimit,
16 maxSplit
17);1List<LocationAndPositions> locationAndPositions = SourcingUtils.loadPositions(
2 SourcingExecutionParameters.builder()
3 .context(context)
4 .sourcingContext(sourcingContext)
5 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
6 .build(),
7 sourcingCriteria,
8 virtualCatalogueRef,
9 locations
10);1SourcingProfile profile = SourcingUtils.loadSourcingProfile(context);
2if (profile != null) {
3 SourcingPlan plan = SourcingUtils.findPlanBasedOnStrategies(
4 SourcingExecutionParameters.builder()
5 .context(context)
6 .sourcingContext(sourcingContext)
7 .profile(profile)
8 .positionStatuses(ImmutableList.of(Constants.Status.ACTIVE))
9 .build()
10 );
11}1List<OrderItem> unfulfilledItems = SourcingUtils.getUnfulfilledItems(sourcingContext);1String networkRef = SourcingUtils.getNetworkRef(profile, strategy);1String catalogueRef = SourcingUtils.getVirtualCatalogueRef(profile, strategy);1Integer maxSplit = SourcingUtils.getMaxSplit(profile, strategy);1SourcingContext sourcingContext = SourcingContextUtils.loadSourcingContext(
2 context,
3 SourcingUtils::getUnfulfilledItems
4);1// Create fulfillments from the sourcing plan
2List<Fulfilment> createdFulfilments = OrderUtils.createFulfilments(
3 context,
4 sourcingContext,
5 plan.getFulfilments()
6);1// Set fulfillment types based on business rules
2OrderUtils.fillFulfilmentType(sourcingContext, fulfilments);1// Get unfulfilled items
2List<OrderItem> unfulfilledItems = SourcingUtils.getUnfulfilledItems(context);
3
4// Find partial fulfillment
5Optional<Fulfilment> partialFulfilmentOpt = SourcingUtils.findHighestValuePartialFulfilment(
6 locationAndPositions,
7 unfulfilledItems,
8 Collections.emptySet() // No excluded locations
9);
10
11// Calculate remaining items after partial fulfillment
12if (partialFulfilmentOpt.isPresent()) {
13 Fulfilment partialFulfilment = partialFulfilmentOpt.get();
14 List<OrderItem> remainingItems = OrderUtils.itemsMinusFulfilments(
15 unfulfilledItems,
16 Arrays.asList(partialFulfilment)
17 );
18}1Location location = LocationUtils.getLocationByRef(context, "store-123");1List<Location> networkLocations = LocationUtils.getLocationsInNetwork(
2 context,
3 "network-001"
4);1List<String> networkRefs = Arrays.asList("network-001", "network-002", "network-003");
2List<Location> allLocations = LocationUtils.getLocationsInNetworks(context, networkRefs);1Location storeLocation = LocationUtils.getLocationByRef(context, "store-123");
2Location customerLocation = LocationUtils.getLocationByRef(context, "customer-location");
3
4if (storeLocation != null && customerLocation != null &&
5 storeLocation.getPrimaryAddress() != null && customerLocation.getPrimaryAddress() != null) {
6
7 double distance = LocationUtils.distanceInMetres(
8 storeLocation.getPrimaryAddress().getLatitude(),
9 storeLocation.getPrimaryAddress().getLongitude(),
10 customerLocation.getPrimaryAddress().getLatitude(),
11 customerLocation.getPrimaryAddress().getLongitude()
12 );
13
14 // Check if within delivery radius (e.g., 50km)
15 if (distance <= 50000) {
16 context.action().log("Location is within delivery radius");
17 }
18}1// With ETA calculation
2EtaCalculator etaCalculator = (sourcingPlan, fulfilment) -> {
3 // Calculate and return ETA for the given fulfilment
4 return ZonedDateTime.now().plusDays(3);
5};
6
7FulfilmentOptionsUtils.createFulfilmentPlan(context, sourcingPlan, sourcingContext, etaCalculator);
8
9// Without ETA calculation
10FulfilmentOptionsUtils.createFulfilmentPlan(context, sourcingPlan, sourcingContext, null);