@entur/add-customers-to-offer-configurations
Adds customers with possible entitlements to offer configurations so that they
can be reserved using reserve-offers
in the Entur Sales System.
Background
In the Entur Sales system, if an offer requires one or more entitlements to be
purchased, the offer configurations that refer to this offer need to contain a
customer with the said entitlements. And for all other offers, you can still
supply a customer to an offer configuration if you want to attach that customer
to the resulting order lines.
Calls to reserve-offers
will fail if you supply an offer configuration with a
customer with entitlements other than exactly the entitlements that are
required to purchase the specified offer.
Installation
Using yarn:
yarn add @entur/add-customers-to-offer-configurations
Using npm:
npm install @entur/add-customers-to-offer-configurations
Usage
The module has a default export:
export default function addCustomersToOfferConfigurations(
customers: Customer[],
offerConfigurations: OfferConfiguration[],
offers: Offer[]
): OfferConfiguration[];
And it is imported the way you import any other default import:
import addCustomersToOfferConfigurations from "@entur/add-customers-to-offer-configurations";
If you call this function supplying the correct data (see next parapgraph), the
function should be able to assign customers to the offer configurations in the
best possible way, supplying exactly the entitlements that the offers require
for purchase.
The OfferConfiguration and Customer types are as found in
the Reserve Offer API.
The Offer type is as found in
the Offers API.
You will find examples of all of these types in the
test/data/
folder. For your convenience, here is one example
for each type, as well as example output:
Travelers and Customers
Note that throughout the documentation, we will refer to a "traveler" a lot,
which refers to the Traveler type from the Offers API as well as the real-world
concept of a named or an anonymous traveler. A Traveler and a Customer refer to
the same person if Traveler.id is the same as Customer.customerId. For example,
the
example customer
looked as follows when it was used to request offers:
{
id: "3537054",
userType: "ADULT",
products: [
{ id: "NSB:EntitlementProduct:levelA1", version: "NSB:Version:V1" },
],
}
Use cases
The addCustomersToOfferConfigurations
function has been designed to work for
all reasonable use cases we could think of. It should therefore work with …
- … offerConfigurations spanning several stretches (i.e. service journeys).
- … offers that require entitlements and not.
- … groups containing both anonymous and named travelers.
- … offer configurations with supplement products.
See examples of usage in
the corresponding test file.
Caveats
Known shortcomings
Due to
the way the assignment problem algorithm works,
the module's support for offer configurations that require multiple customers is
limited. The algorithm is only guaranteed to work if such offers can be
pruned as safe bets.
You can only add customers that were present in the offers request
The algorithm uses traveler IDs contained within the offers to attach customers
to them (see Travelers and customers). The offers'
traveler IDs are subsets of the traveler IDs that were supplied when requesting
the offers. In other words, given a customer where customerId === "123"
, the
algorithm will only be able to attach this customer to an offer that was
requested with a traveler with id === "123"
in its request body. As long as
you keep the group of travelers unchanged from requesting offers until reserving
them, which is standard procedure, you should be fine.
Use at your own risk, and tell us when things go wrong
We, the authors, have chosen to share this code because solving this problem
took long. Sharing our solution will save time and money for Entur as a whole
(as most clients accessing the Entur Sales System will have to solve the same
problem).
We have made our best effort to ensure that our package solves the problem
correctly and that the documentation (readme, code comments and tests) gives a
fair impression of the risks and rewards of using the package. But we do not
control of neither the Offers nor Reserve Offer API, so API changes may
invalidate our assumptions and design choices. And we may have made honest
mistakes.
If you ever experience problems, please report them to
support@entur.org and help fixing the problem in any
way you can (file a PR if you work inside Entur, or just mail us your
suggestions)
How the module works
To solve the problem, we first assume that each stretch in the journey (each
service journey) is independent of the others. That is to say, there is no
information about which customers were assigned to what offerConfiguration for
some earlier or later stretch of the journey that has any impact on how they
should be assigned to the current stretch.
After making that assumption, the problem is solved by two algorithms in
succession.
Pruning safe bets
If an offer has a minimum amount of travelers equal to the travellers it is
meant for, assigning these customers to the offer's corresponding offer
configuration is considered a safe bet. The function assignUsingSafeBets handles
this.
This algorithm was invented to cope with group tickets, in a single ticket and a
single fare product covers multiple travellers. We could not come up with an
elegant modification to assignCustomersUsingMinimumCostFlow, but this algorithm
seems to remove all offer configurations that would warrant a modification from
its input.
Transforming the problem to a graph
Optimally assigning customers to offer configurations is an
assignment problem
(specifically, a balanced assignment problem), which is solvable as a
minimum-cost flow problem.
We construct a flow network representing the problem in the following way:
- Every traveler and offer configuration on this service journey is considered
a node.
- There is also a start node and an end node (a convention in flow problems)
- An edge goes from the start node to each traveler.
- An edge goes from a traveler to an offer configuration if the traveler is in
the offer's traveler mapping.
- Edges from anonymous travelers to an offer have a higher cost than those
from named travelers to the same offer (see detailed explanation below).
- An edge goes from each offer configuration to the end node.
- Its cost is equal to the cost of the offer configuration.
- All edges have capacity 1, except those going from an offer configuration
(whose capacity is equal to the offer configuration's count).
Example
The following illustration shows a problem with three offer configurations and
three travelers. Offer configurations oc1
and oc2
can be used by all
travelers, while oc3
is only available to t3
.
One optimal assignment of customers to offerConfigurations has t3
assigned to
oc3
, t1
to oc1
and t2
assigned to oc2
. The only other possible,
optimal assignment is to switch t1
and t2
around.