Skip to content
Orbit GroundControl home
Orbit GroundControl home

Products

Overview

With Products in Orbit, you can define the logistical products you offer to your customers, partners and suppliers. Similar to how parcel services offer various well-defined shipping products for parcels (Economy, Express, Courier, Premium, etc.) based on factors like size, weight, price, or transit times, Products in Orbit combine timing calculations, routing, feasibility assessments, commercial terms, and line-item based pricing. Orbit Products are applicable to all transport and delivery processes, not limited to specific use cases like parcels, last mile, or FTL/LTL.

Products are a powerful and flexible tool that allows you to accurately reflect your logistical and commercial offerings on the Orbit platform - in terms of pricing and service levels. Products can be used for selling (shipper-facing) and purchasing (carrier-facing) of transports.

Each Product has four key logic components, along with some additional configuration: Context, Feasibility, Pricing, and Scheduling. These logic components are called ProductBricks.

ProductBricks logic is defined using the TypeScript programming language to provide maximum Ïflexibility while preserving maintainability. Orbit provides a feature-rich, built-in code editor that includes advanced functionalities such as code-completion and syntax highlighting. Orbit’s code editor is specifically designed to assist developers in efficiently managing and writing ProductBrick code.

While code is an excellent way to represent logic, most product logic also needs to access structured data at some point. This can be a zoning table with prices, a list of supported countries or simply a bunch of multipliers to use when calculating a price (e.g. for distance based pricing). Orbit DataPools provide the ability to store structured data and easily access them from within the logic of ProductBricks.

In the following docs article you will get detailed insight into Orbit Products and their components as well as some examples and snippets to get you started with products.

Creating and Managing Products requires basic programming knowledge. Specifically knowledge about the TypeScript or Javascript programming languages. If you want to learn the basics of TypeScript we recommend the following online course: https://www.codecademy.com/learn/learn-typescript

If you need any assistance with defining Orbit Products, please do not hesitate to reach out to your Orbit representative. They are available to provide you with the necessary support and guidance. Whether you have questions about configuring Product settings, integrating various functionalities, or troubleshooting any issues, your Orbit representative is there to help.

Key Concepts and Features

ProductBricks

ProductBricks are the building blocks of Products in Orbit. There are four types of ProductBricks:

  1. Context

    • The context brick runs prior to all other bricks of a product. It generates a context object that is accessible by all other bricks via the context parameter.

    • In general the context brick should contain logic that is shared by or used in multiple other bricks. Typically this includes: calculating a route, finding the correct vehicle class, etc pp.

    • A product must contain exactly one context brick.

  2. Feasibility

    • Feasibility bricks determine whether the product can be used to create an offer for the transport request under given conditions.

    • Feasibility checks can differ vastly for different use cases. Examples include: Checking maximum load dimensions or weight, checking supported origin or destination countries, etc pp.

    • A feasibility brick generates a boolean (true/false) value signalling feasibility and optional error messages to explain to the user why the product is not feasible.

    • A product can contain multiple feasibility bricks, the results of which are combined using logical AND for feasibility values and list merging for error messages.

  3. Pricing

    • Pricing bricks calculate the price of a product for a given transport.

    • In many cases pricing logic is either based on distance and/or time of the calculated route or a zoning table stored in a DataPool. But pricing bricks also allow pricing logic to have a completely different structure.

    • A pricing brick generates a list of line items. Line items are very similar to the accounting positions on an invoice. Each line item consists of count, name, monetary value and tax percentage.

    • A product can contain multiple pricing bricks, the generated line items are combined using list merging. The sum of all line items from all the pricing bricks of a product represent the price of the product for a given transport.

  4. Scheduling

    • The scheduling brick determines the available schedule for a transport, particularly applying to start and end times and the runtime of the transport.

    • Scheduling bricks generate multiple different scheduling options with varying granularities (e.g., exact datetime, time window, etc.).

    • The generated scheduling options are used to display a schedule picker to the customer when booking a transport. The customer selects a specific scheduling option from all available options generated by the scheduling brick.

    • A product must contain exactly one scheduling brick, as they are not combinable.

    • Note: The scheduling brick is ignored when booking a transport through the TransportComposer in MissionControl or via the Orbit API.

Each ProductBrick is defined using TypeScript code in Orbit's built-in code editor. The editor provides features like syntax highlighting and code completion to assist in writing clean and functional code.

The code to calculate products is executed in an isolated execution environment providing a high degree of security.

DataPools

DataPools are collections of structured, tabular data accessible by multiple ProductBricks. They are ideal for storing information such as a list of vehicle classes or a zone-based price matrix. By centralising this data, DataPools ensure consistency across different Products and simplify the maintenance of pricing information.

To use a DataPool within a ProductBrick, you must first link the two together at the bottom of the form for editing a ProductBrick. Once linked, a DataPool can be easily accessed in ProductBrick functions via its name, which is suffixed by a random constant on the data input parameter. Our code editor will make linking a DataPool easy utilising Orbit’s code completion & suggestions.

To facilitate easy retrieval of various data types and formats from DataPools, Orbit provides several utility methods. For more information and examples, please refer to the documentation below.

Products

Products are the objects that this is all about. It is what is offered to a customer (Shipper) or service provider (Carrier). A product is a combination of several ProductBricks together with some metadata and commercial terms.

A product can be sold to a customer or used to purchase a service from a service provider (e.g. Carrier). It is also possible to use the same Orbit Product for sales and purchasing. To reflect these use cases a product can be created in one of three categories: Purchasing, Sales or Sales & Purchasing. The category controls at which points and to which user groups the product is displayed in the Orbit system:

  • Sales products can be used to calculate a price in Orbit TransportComposer when creating an Order. They can also be offered to customers in Orbit Hub.

  • Purchasing products can be used to calculate costs in the TransportComposer when creating a Tour. They are also used everywhere a tour is created or significantly edited (e.g. when routing a Shipment or optimising a Tour).

Versioning

  • Products, ProductBricks and DataPools come with time-based versioning functionality. This means that old versions of products remain available and planned updates to price models are easy to implement.

  • Every edit to a Product, ProductBrick or DataPool creates a new version with a validity date that indicates the start of the validity of this product. When a product is requested the version valid at the time of the start of the transport is used to retrieve the Product version valid at that point in time. This means the start time of the transport is used as the timestamp to choose the product version.

  • On Orders and Tours which are priced using a product, the used product version is displayed on the detail pages in MissionControl together with the Product name to allow for a maximum of transparency.

Availability Restrictions

  • If, where and for whom products are available can be controlled by availability restrictions. These are given in the form of queries that check against values of Carriers (for purchasing products) or Shippers (for sales products) and their respective users and teams.

  • If no restrictions are specified, the product is available for all Shippers and Carriers.

  • When restrictions are specified, a product is only available if all specified conditions are met. It's important to note that these restrictions are applied only when the respective Shipper or Carrier is known at the time of product calculation. For example, this occurs when a shipper is selected in the TransportComposer. If no shipper or carrier is selected, the product remains available despite any defined restrictions.

  • Products in Hub and TransportComposer have an additional filter layer for products:

    • To use a product in Hub it needs to be explicitly activated in the Shop Settings (these settings can be adjuste in MissionControl) for that shop. After the product is activate there, additional availability rules still apply if they are configured. Unlike in MissionControl, unavailable or disabled products are not shown in Hub. A shipper that does not pass the availability filters for a product, does not see this product in Hub.

    • To use a product in TransportComposer, it needs to be explicitly activated in the associated TransportShape. After the product is activated there, additional availability rules still apply if they are configured.

Walkthrough

In this walkthrough, we will create a simple Product to explain core concepts in detail. Our Product will include:

  • ContextBrick with routing function for calculating the distance and duration of a route

  • PricingBrick that generates a line item for the base price

  • DataPool with variables used for price calculation

  • Simple FeasibilityBrick checking for load limitations

  • SchedulingBrick that takes route duration into account

In additional steps, we will demonstrate how we can enhance our product with the following functionality:

  • Add zone-based pricing with a zoning table from the data pool

  • Dynamically react to shipper or carrier properties

1. Calculating a route in a ContextBrick

The product we are creating in this walkthrough will calculate prices and runtimes based on the route calculated for the requested transport. We will do this in the ContextBrick of our product.

  1. In the “ProductBricks”-Tab in the settings area of MissionControl, create a new ContextBrick by clicking the “Create ProductBrick” button in the ContextBrick section. Enter a name and description for your ProductBrick. There is no need to link a DataPool for now. Click “Create”.

  2. Find your newly created ContextBrick in the list and click the “expand” button. Click “open code editor” to edit the logic. Unlock the code editor by clicking on the lock icon in the bottom left corner of the editor window. We are now ready to create the logic for our product brick by editing the bricks code. Notice how the first and last row of the code can not be edited as these designate the format of the bricks function. Our custom code goes into the body of this function.

  3. To calculate a route we first need to create a route request. This request contains the start and end points and some configuration options that the system needs to calculate a route. The request is just a data object in a certain format and we could create it from scratch in our code. But it is easier to do this using the buildRouteRequest utility method. Add the following code into your brick:

const routeRequest = utils.route.buildRouteRequest( input.request.tour?.stops.map((s) => s.geocoded) ?? [], { transportMode: "truck" } );

This code passes the coordinates for all stops from the incoming transport request as the first argument to the buildRouteRequest method. The second argument is the options object and sets the mode of the transport to truck to tell the routing service to respect road restrictions for this vehicle class. You can add additional options to the object.

  1. Now that we created the route request we need to send it to our route service to get the route. Add the following code below the code block added before:

const [route] = await utils.route.calculateRoutes([routeRequest]);
  1. We retrieved a route object from our routing service. The route object contains information like distance, duration and so on. We will extract and use this information in the following steps. For now we will return the route from the context brick, so that all other bricks (e.g. PricingBrick) can access and use the route data. Add the following code right before the closing brackets of the function:

return { isFeasible: true, result: { route, context: {}, }, };

The context function has a fixed return format that contains two types of information:

  1. The isFeasible feasibility flag to signal if the requested transport is feasible with this product. As we are not doing any feasibility checks in our context brick at the moment we are returning the fixed value true.

  2. The result of the context brick that is provided to all other bricks. This can contain a route in the designated route key and a other data (which is empty right now) in the context key.

2. Simple Feasibility

Now that we have generated the context for our Product which contains route information, we will implement a simple feasibility check. Lets say our Product is feasible if two conditions are met: 1. the total transport distance is below 1000km and 2. the total weights of all loads is below 27t. Of course in most cases these feasibility checks will be more comprehensive. But we will stick with these simple checks for now. Lets implement them:

  1. In the “ProductBricks”-Tab in the settings area of MissionControl, create a new FeasibilityBrick by clicking the “Create ProductBrick” button in the FeasibilityBrick section. Enter a name and description for your brick. There is no need to link a DataPool for now. Click “Create”.

  2. First we are going to make sure that the transport distance is below 1000km. We can access the route we generated by accessing the context parameter that is passed into our function. Because route objects are complex and contain a lot of complex data, Orbit provides utility functions to make working with routes easier. One of these utility functions is getVehicleSectionDistanceSum which takes a route object as input and outputs the total route distance that needs to be travelled by vehicle (excluding route sections by ferries etc). We are going to use this function to get the total route distance and check if it is under 1000km. Note that distances in orbit are usually given in meters, we need to bear this in mind when comparing. Add the following block to the feasibility function:

const distance = utils.route.getVehicleSectionDistanceSum(context.route!.routes[0].sections); const isDistanceBelowLimit = distance <= 1000 * 1000;
  1. Now that we have calculated our first condition, lets move on to the second: total load weight must be below 27t. Because a transport can have multiple stops with loading and unloading actions it can be difficult to find out what the maximum load weight is for a transport. Again, Orbit provides a utility method to make this easier. This utility method is called maxWeightForStopCountAndLoads and takes in the number of stops a transport has and the loads transported and outputs the maximum weight. We will use this utility to calculate the maximum and check if it is below our limit of 27t. Weights in Orbit are given in kg, so we have to convert tons to kg before comparing. Add the following code to the feasibility brick right below the code added before:

const maxTotalWeight = utils.loads.maxWeightForStopCountAndLoads( input.request.tour.stops.length, input.request.tour.loads ); const isWeightBelowLimit = maxTotalWeight <= 27 * 1000;
  1. Great! We now have calculated our two conditions and saved them in the variables isDistanceBelowLimit and isWeightBelowLimit. We now want to signal, that our product is feasible if both conditions are met. To do this we combine the two conditions using the logical AND operator && and return the value in the isFeasible key in our function.

return { isFeasible: isDistanceBelowLimit && isWeightBelowLimit };

Perfect. We created a simple feasibility function that checks two conditions.

3. Parameterised Pricing

We have already created a context containing route information and handled the feasibility of our Product. Now we can get to the core function of our Product logic: price calculation. We will create a DataPool with the constants used in the calculation of our price. We will then access this data from within our logic and use it to calculate the price. Let’s go.

  1. In the “DataPools”-Tab in the settings area of MissionControl, create a new DataPool by clicking the “Create DataPool” button. Enter a name and description for your DataPool. Click “Create”.

  2. To fill the DataPool with the data we need, click the “view & edit”-button in the top right corner of our newly created DataPool. A window containing the DataPool’s data will open. Use the lock icon in the bottom left corner to unlock the DataPool and enable editing.

  3. For the simple price model we are going to create, we only need two constants: the price per kilometer and the current fuel floater. It is possible to define much more complex data in DataPools, including lists and matrices (e.g. zoning tables or class based price lists). We are going to demonstrate this in a different example. For now enter the two constants next to their names into the DataPool. You DataPool should look like this:

Screenshot 2024-07-16 at 15.27.22.png
  1. Click the save button to save the DataPool.

  2. We are now going to create the PricingBrick and link the DataPool containing our constants. In the “ProductBricks”-Tab in the settings area of MissionControl, create a new PricingBrick by clicking the “Create ProductBrick” button in the PricingBrick section. Enter a name and description for your brick. From the “DataPools” dropdown menu, select the DataPool we just created to link it with the brick and click “Create”.

  3. Open the code editor for our newly create brick and start editing it. Linked DataPools can be accessed via the data parameter and their name suffixed with a random identifier. So your DataPool is accessible using a notation like data.test_QXGL. To make it easier to access data store within a DataPool, orbit provides two utility methods: getSheetColumns and getSheetRows. These utilities extract specified columns (or rows) from a sheet into a typed matrix. We want to extract the column “B” into a list (1D matrix) of floating point numbers. Because we want to extract a column, we are going to use getSheetColumns. Add the following block of code to the body of the PricingBrick, replacing test_QXGL with the identifier of your DataPool. Orbit code editor will warn you, if the identifier you are using does not exist.

const constants = utils.dataPools.getSheetColumns( data.test_QXGL["Sheet 1"], { startRow: 0, endRow: 1, parser: parseFloat }, 1 )[0];

The first argument of getSheetColumns is the sheet we want to read the data from. Each DataPool contains multiple sheets, similar to a spreadsheet document. The default name for the first sheet is “Sheet 1”, but this name can be changed in the bottom left of the DataPool editor where you can navigate between sheets. The second argument is an object containing options. Because we only have two constants, we only need the first two rows of our document. This is given by setting the start and end indices of the extraction to 0 and 1. Because we know that our constant values are floating point numbers, we are passing the default float parser parseFloat as parser. The last argument is the column index. When we added the data to the DataPool we put the names in the first column and the actual values in the second. That is why we are extracting the second column (index 1) here. Because getSheetColumns can be used to extract multiple columns, it returns a list of columns (array of arrays). We only extracted one column and are only interested in the data contained there. To access only the first column we are suffixing the function call with an access call to the first index [0].

  1. Next we extract the two constants from the column of constants that we got in the step before. We know they are the first and second element in the column. We can use the following line of code to do this:

const [pricePerKm, fuelFloater] = constants;
  1. To get our final price we need to get the total distance that needs to be driven for this transport. We extract it like we did in our feasibility function earlier, using the utility method getVehicleSectionDistanceSum :

const vehicleDistance = utils.route.getVehicleSectionDistanceSum( context.route!.routes[0].sections );
  1. Now we have gathered all information we need to calculate the price. Add the following line of code:

const price = Math.round(pricePerKm * vehicleDistance / 1000 * (1 + fuelFloater / 100));

This might seem complicated at first, but it is actually pretty straightforward: To get to our end price (in cents) we multiply the price per km with the kilometers travelled (derived from the distance in meters divided by 1000) and multiply with a fuel floater factor that we obtain by converting the percentage value into decimal form (dividing by 100) and creating a factor for multiplication (by adding 1). Our result is in cents, so we round to a whole number because there are no fractions of cents. This is the complete price calculation logic and we are almost done with our pricing brick.

  1. All that is left is to return the calculation result from our brick. Monetary values in Orbit are given as line items. A pricing brick always returns a list of line items. In this case the list only contains one item: the base line item with the price value we calculated. Add the following code before the closing brackets of the bricks function code. You can adjust the name, vatPercentage and subtype values if needed.

return [ { amount: price, vatPercentage: 19, name: "Grundpreis", subtype: "base", type: "base", count: 1, }, ];

Great! We created a parameterised pricing function. Now if we want to adjust the fuel floater or price per km values we do not need to edit the logic again. We can simply update the values in the DataPool and the brick will use them. In combination with time based versioning we can plan changes in e.g. the fuel floater ahead of time and do not need to update values right when they go into effect.

4. Simple Scheduling

We already implemented most of the logic necessary for our Product. The last missing piece is the SchedulingBrick that tells the system in which time ranges pickup and delivery can be scheduled. We will keep the scheduling logic simple and focus on two values: the earliest possible pickup and the minimum runtime for our transport. Let’s go!

  1. In the “ProductBricks”-Tab in the settings area of MissionControl, create a new SchedulingBrick by clicking the “Create ProductBrick” button in the SchedulingBrick section. Enter a name and description for the SchedulingBrick. There is no need to link a DataPool to this brick. Click “Create”.

  2. First let’s calculate the earliest possible pickup for our transport. Let’s say the earliest we can pickup is in 4 hours, but only if that point in time is within our regular operating hours that we will define on the product later and only if its on a weekday we are operating on. Because time calculations can be quite complex, Orbit provides a bunch of utility methods to help you with this. In this case we will use the function getNextBookableTime to which we will pass the earliest possible pickup (now plus 4 hours, converted to seconds, the standard unit used for timestamps in Orbit), the disabled weekdays and the operating hours from our product. The function will then calculate the earliest point in time that meets the criteria defined above. Add the following code to the body of the SchedulingBrick:

const now = utils.datemTime.DateTime.now(); const earliestPickup = utils.dateTime.getNextBookableTime( now.plus({ hours: 4 }).toSeconds(), input.product.disabledWeekdays, input.product.operatingHours );
  1. In addition to the earliest possible pickup, we need the minimum runtime for the transport. We will extract this information from the route we calculated in our ContextBrick earlier. To do this easily, Orbit provides a utility function called getTotalRouteDuration. We are using it in the following line of code:

const duration = utils.route.getTotalRouteDuration(context.route?.routes[0]);
  1. That’s it. We have all the information we need to create a simple scheduling configuration. Add the following code right before the closing brackets of the SchedulingBrick:

return { earliestPickup: Math.round(earliestPickup.toSeconds()), minRuntime: Math.round(duration), operatingHours: input.product.operatingHours, disabledWeekdays: input.product.disabledWeekdays, otherOfferSelectedText: input.product.otherOfferSelectedText, pickupOptions: [{ name: "Zeitpunkt", id: "fix", granularity: "time" }], deliveryOptions: [{ name: "Zeitpunkt", id: "fix", granularity: "time" }], };

We are returning the two values we calculated in the fields intended in the schedule config, both rounded to a whole number because we do not want to take second fractions into account. Additionally we are forwarding operatingHours, disabledWeekdays and otherOfferSelectedText from the product directly into the config. The last two fields specify the options available when specifying a time window. We are using a default setting for a fixed point in time for both pickup and delivery. Thats it. We defined our scheduling config and now have all that we need to create the Product.

5. Tying it all together

We are now ready to create our Product. Let’s go:

  1. In the “Products”-Tab in the settings area of MissionControl, create a new Product for sales and purchasing by clicking the “Create Product” button in the sales and purchasing section. This product can be used for sales and purchasing, but as of right now will display the same prices for selling and for purchasing. In a real world scenario this would need to be adjusted either by changing pricing logic or defining separate products for sales and purchasing.

  2. Add a name and description to the Product. These fields can be localised so that operators and customers can see the texts in their selected language.

  3. The tooltip and info fields are displayed to customers when using Orbit Hub. We will skip these fields for now, but it is highly recommended to configure these fields when usage in Orbit Hub is intended. We will skip configuring availability restrictions as well for now, as this is an advanced feature.

  4. In each of the four ProductBrick sections that follow, add the respective brick that we created earlier.

  5. In the metadata section we will configure the operating hours and disabled weekdays. We are operating from 7:00 until 18:00 every day except Saturday and Sunday. Click “Create”.

  6. Congratulations! You just created your first Orbit Product. You are now able to use the Product at several integration points in Orbit, e.g. when calculating the price for a tour in the ShipmentRouter. Usage in Hub and TransportComposer require additional configuration in the shop config or associated TransportShape.

6. Zone based pricing

Work in progress

7. Adjust pricing based on shipper properties

Work in progress

Utility Methods

The following utility methods are available in the utils object and can be accessed within ProductBricks to perform various calculations and data manipulations.

Route

getVehicleSectionDistanceSum(sections: RouterSections[]): number

Function to calculate the distance that needs to be driven by a vehicle on a route. This excludes route sections that are travelled by e.g. ferry. This is the standard way to calculate the distance for a route. Returns the total distance in meters.

getTransitSections(r?: HereRoute): HereRouterComponents["schemas"]["TransitSection"][]

Function to extract transit sections from a given route. Returns an array of transit sections or an empty array if no route is provided.

getTotalRouteDuration(r?: HereRoute): number

Function to calculate the total duration of a route. Returns the total duration in seconds, or 0 if no route is provided.

groupRouteSectionsForWaypoints(sections: HereRouterComponents["schemas"]["RouterSection"][], originalWaypoints: Location[]): HereRouterComponents["schemas"]["RouterSection"][][]

Function to group route sections based on waypoints. Returns an array of section groups, where each group represents the sections needed to reach a waypoint.

buildRouteRequest(stops: GeocodedStop[], options: { transportMode: string }): RouteRequest

Function to build a route request object for a given set of stops and transport mode.

DataPools

getSheetColumns<T = string>(sheet: string[][], options: Object, ...columnIndices: Array<number | number[]>): T[][]

Function to extract and optionally transform specified columns from a sheet into a typed matrix.

getSheetRows<T = string>(sheet: string[][], options: Object, ...rowIndices: Array<number | number[]>): T[][]

Function to extract and optionally transform specified rows from a sheet into a typed matrix.

matrixToJson(data: string[][]): object[]

Function to convert a matrix of data into an array of JSON objects.

parseObjectsFromSheetRows<T>(schema: ZodType<T>, sheet: string[][], options: Object, rowIndices: number[]): T[]

Function to parse and validate objects from sheet rows using a Zod schema.

Conversions

kmhToMetersPerSecond(kmh: number): number

Function to convert speed from kilometers per hour to meters per second.

Loads

hasDangerousGoods(loads: BaseLoad[]): boolean

Function to check if any of the given loads contain dangerous goods.

calculateLoadPlan(width: number, length: number, loads: BaseLoad[]): { doesFit: boolean, doesFitConfidently: boolean }

Function to calculate if the given loads fit within the specified dimensions.

maxWeightForStopCountAndLoads(stopCount: number, loads: Load[]): number | undefined

Function to calculate the maximum weight of loads at any stop for a given number of stops and loads.

Tools

emptyStringToUndefined(value: string): string | undefined

Function to convert empty strings to undefined.

z

Zod library for schema validation. See Documentation here.

DateTime

DateTime

DateTime object for date and time operations. See Documentation here.

Duration

Duration object for time duration operations. See Documentation here.

getNextBookableDate(from: number, disabledWeekdays: number[], direction: "forward" | "backward" = "forward", minDaysDistance = 1): DateTime

Function to get the next bookable date based on given parameters.

getNextBookableTime(from: number, disabledWeekdays: number[] = [], openingHours: { from: TimeObjectUnits; to: TimeObjectUnits } = { from: { hour: 0, minute: 0, second: 0, millisecond: 0 }, to: { hour: 0, minute: 0, second: 0, millisecond: 0 } }): DateTime

Function to get the next bookable time based on given parameters, considering opening hours and disabled weekdays.

FAQs and Troubleshooting

Q: How do I know if my code will work? A: We recommend using the code in the basic product as a starting point to customise each ProductBrick according to your needs. The code editor’s inbuilt syntax highlighting and error indication should be able to help you in most cases. If you need further assistance please don’t hesitate to reach out to our customer support.

Q: My product is not displayed at an integration point where I expect it to be displayed (e.g. in Hub, ShipmentRouter, etc). How can I find out why? A: 1. check the product category (sales, purchasing) 2. check access rules 3. if using hub, check if the product is selected for offer in the shop settings. if using TransportComposer check TransportShape. 4. check the validity of Product and transport date. If transport date is before date of first version of the Product, Product is not displayed.