Benefits

Concepts

Howtos

Concepts

In this section:

Loyalty programs

A loyalty program within the Entur systems is an extension to a product. Thus, a loyalty program will always have a unique product attached. The product may give fare reductions, but this is managed elsewhere in the Entur systems. In these cases, this document will use the term "discount rights", as in "the customer has discount rights from the XX product"

Loyalty programs are divided into three main types:

  • time-limited
  • coupon-limited
  • points-limited

All types have a first date of validity and an expiration date (even if it may be null).

This is an overview of the data structure: class diagram

Time-limited loyalty programs

Identifier: "TIMED" An example of a time-limited loyalty program is "a customer card". For this type of loyalty program Entur supports registering any discounts the contract has been used to garner. It is not possible to register points-transactions or coupons on contracts for a time-limited loyalty program.

Coupon-limited loyalty programs

Identifier: "COUPONS" An example of a coupons-limited loyalty program is "a 10-coupon daily reduction", where a customer is allowed 10 separate days of discount rights. In this case, the coupons-limited loyalty program is configured to create a contract for a time-limited loyalty program for a given period (here, 24h) from first purchase. On contracts for this type of loyalty program, neither points-transactions or discounts may be registered. However, if configured, usage will be registered on the created coupons-contracts.

Points-limited loyalty programs

Identifier: "POINTS" This type of loyalty program is currently used for gift cards. Contracts for a Points-limited loyalty program can contain a list of points-transactions. The type of the points is specified on the initial deposit, and can not be mixed. It is not possible to register discounts or coupons on a contract for a points-based loyalty program.

Loyalty program versions

Loyalty programs are versioned by date. For each version, these fields can be changed:

  • Product version. If a new product version is available in the products db, you can create a new loyalty program version to match.

  • Version start and end times. A Loyalty Program is valid if there exists a version with startDate before now and endDate after now. If multiple versions have overlapping time periods, the one with the highest versionNumber is considered the CURRENT version. If the startDate is in the future, it is considered to be in DRAFT status. Creating new versions will overwrite the current DRAFT version. If the endDate is in the past, the version is considered DEPRECATED and no new contracts can be created for this version. If all versions are DEPRECATED, no new contracts can be created and no existing contracts will be accessible. In short:

    DRAFT means it is possible to save the loyalty program for further edits or another publish date, but it is not possible to create contracts on the program. CURRENT mean it is possible to create contracts. DEPRECATED means that it is no longer possible to create contracts

  • Descriptions. If the descriptions are not altered when a new version is created, they are copied to the new version.

  • For COUPONS based loyalty programs, you can adjust the couponConfig by making a new version.

Call to create a new version:

POST https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/{loyaltyProgramId}/versions
{
  "descriptions": [
    {
      "languageCode": "NOB",
      "description": "Reis Kundekort gir deg 20% rabatt på alle kjøp"
    }
  ],
  "startDate": "2007-12-03T10:15:30+01:00",
  "endDate": "2007-12-03T10:15:30+01:00",
  "usageValidityPeriod": "P3DT12H30M5S",
  "defaultCouponsLimit": 10,
  "productVersion": "ENT:Version:V1"
}

Response:

{
  "id": 2324,
  "organisationId": 20,
  "internalDescription": "Customer card with 15 % discount",
  "productId": "string",
  "productVersion": "string",
  "startDate": "2019-12-20T09:40:18.213Z",
  "endDate": "2019-12-20T09:40:18.213Z",
  "usageValidityPeriod": "P3DT12H30M5S",
  "defaultCouponsLimit": 10,
  "versionNumber": 2,
  "status": "DRAFT",
  "descriptions": [
    {
      "languageCode": "NOB",
      "description": "Reis Kundekort gir deg 20% rabatt på alle kjøp",
      "createdAt": "2007-12-03T10:15:30+01:00",
      "createdBy": "123e4567-e89b-12d3-a456-426655440000",
      "lastChangedAt": "2007-12-03T10:15:30+01:00",
      "lastChangedBy": "123e4567-e89b-12d3-a456-426655440000"
    }
  ]
}
Usage validity

A loyalty program may define a usageValidityPeriod, governing how long any attached contracts are valid. To avoid ambiguity, only Day and Time units are allowed.

Contracts

A Contract is the connection from one or more customers to a Loyalty Program. A Contract can be considered an instance of a loyalty program. So, for instance, if the loyalty program is points-based and relates to the fictional product ENT:PointsBasedProduct:GiftCard, each Contract can be considered a single gift card. Another example, if we have the product ENT:EntitlementProduct:levelA1 (personnel ticket, silver) and there exists a loyalty program with this product, each Contract can be considered an entitlement for one specific employee or family member.

There are some limitations imposed on the types of thing you can register on a Contract based on the type of the loyalty program it belongs to. Read more about this above. In particular, only POINTS-based Contracts may have transactions, only TIMED Contracts may have OrderLineEvents and only COUPONS-based Contracts may have child Contracts - each representing a used coupon.

Contracts may have Consumers. A Consumer is a Customer, identified by customerNumber and organisationId. The endpoint also accepts customerRef which may be useful for identifying external customers down stream in the sales process. Consumers have two flags:

  • isContractHolder: Whether this consumer is the current contract holder. Only one contract holder can exist for a given contract. Contract holders must be from the same organisation that owns the Contract. It is not possible to add Consumers with isContractHolder=false unless there exists a Consumer where isContractHolder=true.
  • isBlocked: If a Consumer is marked as blocked, it will not show up as a valid contract for this customer when searching. At the same time, since there exists a Consumer for that customer, they can not create a new Consumer. This effectively blocks this customer from using this contract.

A Contract may be created with a set of policies. Policies are values that need to be supplied with correct value to add a consumer to the Contract. For instance, if a Contract is created with a policy that requires any consumers to be called "Anders", only customers supplying this information will be able to consume the contract.

First, we create the Contract:

curl -X POST https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/6/contracts -H"Authorization: Bearer $PARTNER_TOKEN" -H'Content-Type: application/json' -d'
{
  "consumableFrom": "2019-11-01T10:15:30+01:00",
  "policies": [{"key":"name","value":"Anders"}],
  "expirationDate": "2040-11-01T10:15:30+01:00"
}
'

Response:

{
  "uuid": "f1d419bc-e88e-43c9-bf16-6943921a9580",
...
  "policies": [
    {
      "id": 969,
      "key": "name"
    }
  ],
  "contractConsumers": [],
...
}

Attempt to consume with wrong name:

curl -X POST "https://api.staging.entur.io/customers/v2/benefits/contracts/f1d419bc-e88e-43c9-bf16-6943921a9580/contract-consumer" -H"Authorization: Bearer $PARTNER_TOKEN" -H'Content-Type: application/json' -d'
{
  "customerNumber": 1234567,
  "customerRef": "1234567",
  "customerOrganisationId": 1,
  "isContractHolder": true,
  "policyValidationProperties": {
      "name": "Benny"
  }
}'

Response:

{
  "timestamp": "2020-03-13T09:29:04Z",
  "status": 401,
  "error": "Unauthorized",
  "path": "/benefits/contracts/f1d419bc-e88e-43c9-bf16-6943921a9580/contract-consumer",
  "message": "Add contract consumer for contract f1d419bc-e88e-43c9-bf16-6943921a9580 failed. The contract policies didn't match the policies provided in the request.",
  "correlationId": "9d54fd0c-8207-43f7-a4b9-7fb5aa333b46"
}

Attempt to consume with correct name:

curl -X POST "https://api.staging.entur.io/customers/v2/benefits/contracts/f1d419bc-e88e-43c9-bf16-6943921a9580/contract-consumer" -H"Authorization: Bearer $PARTNER_TOKEN" -H'Content-Type: application/json' -d'
{
  "customerNumber": 1234567,
  "customerRef": "1234567",
  "customerOrganisationId": 1,
  "isContractHolder": true,
  "policyValidationProperties": {
      "name": "Anders"
  }
}'

Response:

{
  "customerOrganisationId": 1,
  "customerNumber": 1234567,
  "customerRef": "1234567",
  "isBlocked": false,
  "isContractHolder": true,
  "createdAt": "2020-03-13T09:31:24Z",
  "createdBy": "6BiN61j5CQBeU7w4fa72x6pIqdmhcEO6",
  "lastChangedAt": "2020-03-13T09:31:24Z",
  "lastChangedBy": "6BiN61j5CQBeU7w4fa72x6pIqdmhcEO6"
}

Response codes

Calling contracts/validate-consumptions allows the caller to check if the given contracts are available for the given customers on the given day. The relevant response codes are:

codemessage
3101Ok. All are valid.
3102Some of the passed contracts are unknown.
3103Some of the passed contracts are unavailable on the date of travel.
3104Some of the passed contracts are not available for the customer.
3105Some of the passed contracts are blocked for the customer.
3106Some of the passed contracts are out of coupons and no non-coupon contract was found on this date.

Examples and Howtos

In this section:

How to create a loyalty program

Creating a new Loyalty Program is usually only to be done after some communication with Entur, since it needs to be connected to a product. Here's how it's done then:

  • Find out which product and version you need to target
  • Decide on which kind of Loyalty Program you want to create. Have a look in the section about Loyalty Program types above.
  • Decide when the Loyalty program should be available. Typically, Loyalty programs have long lives.
  • Decide how long a contract should be available as default. It may be several years, for instance for customer cards.
  • Decide if other organisations should be allowed to connect their customers to your contracts. This will make it easier to adapt your loyalty program for new users, but will not drive customers to your sales channels. Only your customers can own a contract, regardless of what you decide here.
  • Decide on the text you want to use to represent this loyalty program to your customers.

Then, call the createLoyaltyProgram endpoint:

curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json"
-d'
{
  "loyaltyProgramType": "TIMED",
  "productId": "ENT:DemoProduct:Demo",
  "productVersion": "ENT:ProductVersion:V1",
  "internalDescription": "Free coffee program",
  "startDate": "2020-03-13T10:26:44.118Z",
  "endDate": "2030-11-22T01:55:56+00:00",
  "usageValidityPeriod": "P365D",
  "descriptions": [
    {
      "languageCode": "ENG",
      "description": "Show your ticket, get free coffee",
      "displayName": "Demo Coffee Program"
    },
    {
      "languageCode": "NOB",
      "description": "Kaffeprogrammet til Entur gir deg gratis kaffe hos lokfører!",
      "displayName": "Demo kaffe-program"
    }
  ]
}'

How to create a contract

Before creating a Contract, make sure you know what you want to create:

  • Do you know which customer is going to be the owner? If so, find the customerNumber and customerReference.
  • Do you know if there is a limiting factor, such as "may only be consumed by my customers" or "Only Jan Petersen may consume this"? If so, add the necessary policies.
  • Do you have a reference you would like to show up in the sales transaction summary if this contract is used in a sale?
  • When should the customers be able to use this contract? Set consumableFrom to this time.
  • Does this contract have a specific expiration date or will the default work? Set expirationDate if it is specific.
  • Is the default coupons configuration ok? If not, specify another number of coupons.

Only a few of the fields are required for creating a contract.

Contracts are then created using the createContract endpoint

Minimum:

curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/6/contracts" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json" -d'
 {
   "consumableFrom": "2020-01-01T00:00:00+01:00",
 }'

When we know more:

curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/6/contracts" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json" -d'
 {
   "consumableFrom": "2020-01-01T00:00:00+01:00",
   "customerNumber": 12341567,
   "customerRef": "SomeCustomer",
   "externalRef": "ABCD-EFGH-1234",
   "policies": [
     {
       "key": "email",
       "value": "kari.normann@example.com"
     }
   ],
   "expirationDate": "2020-12-31T23:59:59+01:00"
 }'

In the example above, the first contract is open to all, not connected to any consumers and only accessible by uuid. The second is already connected to one consumer (as contract holder), and has a policy that only allows a customer who supplies a specific email when attempt to consume. This second contract can also be found via its externalRef or its consumer. The external reference is propagated through the sales channels when used.

How to connect a profile to a contract

A profile can be connected to a Contract in two ways:

  1. It can be the contract owner. This must be a profile from the same organisation that owns the loyalty program and contract.
  2. It can be a contract consumer. This can be a profile from any organisation.

Common for both types of connections is that they must conform to the policy that has been set up for the Contract when the connection is performed.

Also, a major difference here is that a contract only can have one owner. If the owner is removed from the contract, it will no longer be accessible.

To connect to a contract, use the addContractConsumer endpoint:

POST https://api.staging.entur.io/customers/v2/benefits/contracts/2c27bd7e-2b4d-4144-715b-fcbc8fe3807f/contract-consumer
{
  "customerOrganisationId": 20,                           #1
  "customerNumber": 1234567,                              #2
  "customerRef": "073R41D4R",                             #3
  "isContractHolder": true,                               #4
  "policyValidationProperties": {                         #5
    "email": "customer@email.example"                     #6
  }
}

Output:
{
  "customerOrganisationId": 20,
  "customerNumber": 1234567,
  "customerRef": "073R41D4R",
  "isBlocked": false,
  "isContractHolder": true,
  "createdAt": "2019-10-14T10:15:30+01:00",
  "createdBy": "clientId",
  "lastChangedAt": "2019-10-14T10:15:30+01:00",
  "lastChangedBy": "clientId"
}

The input values are:

  1. The organisation of the profile you are connecting
  2. The customer number of the profile.
  3. A customer reference. This field is not required but can be provided. If present, it will be echoed in the responses.
  4. Whether you are trying to register a profile as the Contract Holder. Only one such is possible.
  5. An object containing values for validating your claim to this contract. Depending on the contract, it may have a set of values you need to match to be able to consume it. The keys must match the keys used when creating the contract. Note, the contents of these fields are stored and compared as hashed values, so they may contain gdpr data without issues.
  6. For instance, if the contract requires a specific email, it is sent here.

To get the uuid for this url, you may want to look up via the search endpoint.

How to connect a profile to a personnel ticket right

Personnel tickets are treated slightly different than other contracts. All contracts for personnel ticket loyalty programs are created by the personnel ticket system. They simultaneously create a customer profile and connect it as owner of the contract. All of this happens with the organisation ID 29 (the Norwegian Public Rail Administration) and therefore by default inaccessible for all partners.

To mitigate this, a couple of helper-services have been created. To connect a customer to a personnel ticket contract, there is a [separate endpoint](https://petstore.swagger.io/?url=https://developer.entur.org/static/customers/swagger-customers.json#/Contracts client/claimPersonnelTicket_1).

It is somewhat simpler and will make sure customers are connected correctly in both the new and old personnel ticket regimes. It will look up the customer based on customerNumber, and connect it to the corresponding contracts.

curl -X POST "https://api.staging.entur.io/customers/v2/benefits/contracts/claim-personnel-ticket" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json" -d'
 {
   "externalReference": "ABCD-EF12-3456-GH45",
    "customerNumber": 1234567
 }'

Using contracts for entitlement products in the sales process

To use entitlement products in the sales flow, the customer must exist in Entur customers and it must be connected to one or more entitlement products through loyalty program contracts (see the previous two sections).

Using entitlement products based on contracts is done as part of the sales flow: sales flow with entitlements

In this graph, the actors are:

Fetching entitlements

Example: Currently logged in customer is Anders Jensen. He has customerNumber 1234567. His customer profile is connected to his personnel ticket (personnel ticket reference "ABCD-DEFG-GH12") and his daughters' (Josefine, reference "1234-ABCF-3456")

To fetch entitlements, we call the endpoint:

GET https://api.entur.io/customers/v2/benefits/entitlements/1234567/by-customer-number
[
  {  // Sølv, fritidsbillett
    "productId" : "ENT:EntitlementProduct:levelA2",
    "productVersion" : "ENT:Version:1",
    "contractUUID" : "058a9dfa-187e-4443-8660-29d8879c146a",
    "contractExternalRef" : "ABCD-DEFG-GH12",
    "contractOwnerCustomerNumber" : 4567891,
    "contractOwnerDisplayName" : "Anders",
    "contractOwnerBirthDate" : {
      "year": 1986,
      "month": 11,
      "day": 23
    },
    "contractValidFrom" : "2020-01-01T00:00:00.000",
    "contractValidTo" : "2020-01-01T00:00:00.000",
  },
  {  // sølv, tjenestereisebillett
    "productId" : "ENT:EntitlementProduct:levelA4",
    "productVersion" : "ENT:Version:1",
    "contractUUID" : "058a9dfa-187e-4443-8660-29d8879c146a",
    "contractExternalRef" : "ABCD-DEFG-GH12",
    "contractOwnerCustomerNumber" : 4567892,
    "contractOwnerDisplayName" : "Anders",
    "contractOwnerBirthDate" : {
      "year": 1986,
      "month": 11,
      "day": 23
    },
    "contractValidFrom" : "2020-01-01T00:00:00.000",
    "contractValidTo" : "2020-01-01T00:00:00.000",
  },
  {  // sølv, fritidsreisebillett
    "productId" : "ENT:EntitlementProduct:levelA2",
    "productVersion" : "ENT:Version:1",
    "contractUUID" : "47db2f5f-bf14-4c34-9856-51ba4a213cbe",
    "contractExternalRef" : "1234-ABCF-3456",
    "contractOwnerCustomerNumber" : 4585692,
    "contractOwnerDisplayName" : "Josefine",
    "contractOwnerBirthDate" : {
      "year": 2006,
      "month": 3,
      "day": 2
    },
    "contractValidFrom" : "2020-01-01T00:00:00.000",
    "contractValidTo" : "2020-01-01T00:00:00.000",
  }
]

Please note in the example output:

  • Employees hav at least two entitlements. One for private journeys and one for work travel. These have the same externalRef.
  • Family members and pensioners have just one personnel ticket entitlement - for private travel.
  • The Contract Owner for the personnel ticket entitlements are owned (isContractHolder=true) by a customer profile administrated by the personnel ticket system. This means that the ownerCustomerNumber typically will not refer to the current customer.

Client side rendering

At this point, the sales client must present the relevant choices to the end user and offer an option to choose which ticket rights to use, if any. For each, this is also where it is connected to the userType - ADULT, CHILD, etc. - for each traveller. This list should be filtered based on traveldate and the validity dates of the contracts.

In our example, we could choose to present this as a list of choices:

Voksen0- +
Barn0- +
Anders, Adult, private+
Anders, Adult, corporate+
Josefine, Child, private+

Also, this is the place to determine if any of the travellers get senior citizens rebate.

Using the entitlements to get discounted offers

Once the end user has selected who is travelling, you can query offers with the entitlement products. In the example, the end user has chosen two private travels, one for himself and one for his daughter.

POST https://api.entur.io/sales/v1/offers/search
...the rest of the offers query...,
"travelers": [
    {
        "id": "traveler1",
        "userType": "ADULT"
        "products": [
            {
              "id" : "ENT:EntitlementProduct:levelA2",
              "version" : "ENT:Version:1",
            }
        ]
    },
    {
        "id": "traveler2",
        "userType": "CHILD"
        "products": [
            {
              "id" : "ENT:EntitlementProduct:levelA2",
              "version" : "ENT:Version:1",
            }
        ]
    }
]

Response:

{
  "offers": [
    {
      "id": "8b2cfd32-836a-40f8-b244-d7bef739144d",
      ...

      "travellerMapping": [
        {
          "travelerIds": [
              "traveler1"
          ],
          "maxNumberOfTravelers": 1,
          "minNumberOfTravelers": 1,
          "userProfileId": "NSB:UserProfile:Adult",
          "userProfileIds": []
        }
    },
    {
      "id": "8b2cfd32-836a-40f8-b244-d7bef739144e",
      ...

      "travellerMapping": [
        {
          "travelerIds": [
              "traveler2"
          ],
          "maxNumberOfTravelers": 1,
          "minNumberOfTravelers": 1,
          "userProfileId": "NSB:UserProfile:Child",
          "userProfileIds": []
        }
      ]
    }
  ]
}

Since the offers service has no concept about a customer we need to pass the entitlements for the correct travellers to get the correct prices. Internally, the entitlement product we send in has a connection to a SalesDiscountRight which is applied to the prices before returning from offers. When passed an entitlement product, offers will locate valid SalesDiscountRights and apply them. Which SalesDiscountRights were applied can be read directly from the returned offer in the section called FareProductConfiguration. Any calculated discounts can be found at jpath offers[].salesPackageConfig.fareProducts[].discountRight.parameters.entitlementGiven[]

We recommend the savings are shown to the end user:

Oslo S - Bergen, Vy
Normal price1579
Personnel ticket discount (*)-1579
Seat 45B59
Total59

Here, the (*) may show some text about this kind of discount being reported to the Tax Authorities.

Accepting the offer and payment

Once the offer above is accepted, the procedure for creating the order requires some small changes when using entitlements.

To begin, we need to know if the current logged in user is allowed to use those entitlements. To do this, we need to have that person as creator of the Order:

POST https://api.entur.io/sales/v1/order
{
  ... other order-values ...,
  "contactInfo": {
    "createdBy": {
      "id": 1234567
    }
  }
}

Then, the offer is converted to an order. Here, we need to include who is travelling (so names on personnel tickets can be correct). Also, which contracts and entitlement product were actually chosen for the different travellers. This is done when calling reserve-offers with an appropriate offerConfiguration:

POST https://api.entur.io/sales/v1/reserve-offer
"offerConfigurations": [
  {
    "offerId": "8b2cfd32-836a-40f8-b244-d7bef739144d",
    "customers": [
      {
        "customerId": "4567891",
        "entitlements": [
          {
            "contractId": "058a9dfa-187e-4443-8660-29d8879c146a",
            "entitlementProductRef": {
              "id": "ENT:EntitlementProduct:levelA2"
            },
            "externalEntitlementRef": {
              "id": "ABCD-DEFG-GH12"
            }
          }
        ]
      }
    ]
  },{
    "offerId": "8b2cfd32-836a-40f8-b244-d7bef739144e",
    "customers": [
      {
        "customerId": "4585692",
        "entitlements": [
          {
            "contractId": "47db2f5f-bf14-4c34-9856-51ba4a213cbe",
            "entitlementProductRef": {
              id": "ENT:EntitlementProduct:levelA1"
            },
            "externalEntitlementRef": {
              "id": "1234-ABCF-3456
            }
          }
        ]
      }
    ]
  }
]

Note about the example: Each traveller gets their own offerConfiguration. For each element we specify who the traveller is through the customer object (with customer number from the entitlement), and which contract and entitlement product were used.

This is then used internally to check if the creator is allowed to use the entitlements they are presenting.