Extension Data Constraints

Overview

Extension Data Constraints are a tool to help ensure that extension data fields that are related are persisted in valid combinations. For example, suppose you had a table of vehicle data like this:

Vehicles

Make

Model

Year

Ford

Mustang

2020

Ford

Mustang

2021

Ford

F150

2021

Toyota

Camry

2020

Toyota

Camry

2021

Toyota

Camry

2022

In this case, if the user first chooses Ford as the Make, then you would want to present only Mustang and F150 as options for Model, and not Camry. Likewise, if the user then selects F150 for the Model, they should be constrained to select only 2021 as the Year, and not 2020.

Extension Data Constraints support this sort of constraint building for user interfaces, and can also be used as a validation step to ensure that data created outside of the normal UI also conforms to the constraints desired (though this validation can be bypassed if needed.)

Structure

Data Constraints may be simple (such as constraining the value of a field based on the value of another field on that same element), or complex, where the interdependent fields are on completely different elements within the policy structure. The components used are:

  • Configuration, where you declare dependencies, using a path structure

  • Dependency Maps, which are generated for segments based on the actual data in a segment

  • Filter Evaluation API, which will compute allowed values for fields based on existing or prospective updated data

  • Constraint Tables, which are a special kind of table used for defining valid combinations of data

  • Extended Validation, which can ensure that data on the segment meets constraint requirements, regardless of whether it originated in a UI

Configuration

Each property in a data extension has a constraint field, which defines:

  • The name of the table that contains the valid combinations

  • The column of the table that has valid values for that particular field

  • The where clause, which contains constraints for other columns in the table, or a static list of allowed values

For example, a configuration might include the following field declarations within a vehicle element declaration:

{
  "vehicleMake": {
    "type": "string",
    "constraint": {
      "table": "vehicle_data",
      "column": "make_name"
    }
  },
  "vehicleModel": {
    "type": "string",
    "constraint": {
      "table": "vehicle_data",
      "column": "model_name",
      "where": {
        "make_name": {
          "key": "vehicleMake"
        }
      }
    }
  },
  "vehicleYear": {
    "table": "vehicle_data",
    "column": "year",
    "where": {
      "make_name": {
        "key": "vehicleMake"
      },
      "model_name": {
        "key": "vehicleModel"
      }
    }
  }
}

In this example, we declare that:

  • The vehicle make must be one of the values in the vehicle_data table’s make_name column.

  • The model must be one of the values in the model_name column of that same table, but only including those rows that have make_name matching the make selected by the user.

  • The year must be one of the values in the year column of the table, but only including those rows that have make_name matching the selected make and model_name matching the selected model.

Dependency Maps

After a quote or policy transaction is created, the UI can call the Fetch Dependency Map for Quote or Fetch Dependency Map for Policy Transaction endpoint. The result will be the Dependency Map, which describes the dependencies and constrains that apply for that quote or policy transaction.

A DependencyMap is a nested map that references ConstraintDependency objects, each of which has the same structure as the constraints in configuration. The difference is that each of these is referenced by the actual locator of data for the quote or policy transaction. For example, a dependency map using the above configuration might look like this:

{
  "01HTKMYZ0W7QT7VMSMMCFQNT1Q": {
    "vehicleMake": {
      "table": "vehicle_data",
      "column": "make_name"
    },
    "vehicleModel": {
      "table": "vehicle_data",
      "column": "model_name",
      "where": {
        "make_name": {
          "fieldName": "vehicleMake",
          "staticLocator": "01HTKMYZ0W7QT7VMSMMCFQNT1Q"
        }
    },
    "vehicleYear": {
      "table": "vehicle_data",
      "column": "year",
      "where": {
        "make_name": {
           "fieldName": "vehicleMake",
           "staticLocator": "01HTKMYZ0W7QT7VMSMMCFQNT1Q"
        },
        "model_name": {
          "fieldName": "vehicleModel",
          "staticLocator": "01HTKMYZ0W7QT7VMSMMCFQNT1Q"
        }
      }
    }
  }
}

This map indicates that:

  • The only element that has constrainted data is the vehicle, which has a staticLocator of 01HTKMYZ0W7QT7VMSMMCFQNT1Q.

  • The vehicle make is still dependant on the table data only, as declared in the configuration

  • The vehicle model has the same table dependency but also only rows with a make_name that equal the vehicleMake property for that vehicle will be considered.

  • The vehicle year is similar but will only match rows with a matching make name (like the model), but also where model_name matches the vehicleModel on the element.

Now, with the dependency map, the UI has enough information to ask Socotra what the actual allowed values should be for the make and model fields.

Note

When adding elements, the client should fetch a new dependency map if any of those elements could affect constraint processing in the UI.

Constraint Evaluation API

The UI can use the dependency map, together with the values selected so far by the user, in order to get updates of what values are valid for remaining inputs. This is done by calling the Evaluate Constraints for Quote or Evaluate Constraints for Policy Transaction endpoints.

For example, with the dependency map above, the UI could send this request:

{
  "dependencyMap": { /* as received earlier */ },
  "01HTKMYZ0W7QT7VMSMMCFQNT1Q": {
    "vehicleMake": {},
    "vehicleModel": {},
    "vehicleYear": {}
  },
  "01HTKN7Y5PSYF0D95RWTTZ88QT": { }
}

In this case, the user hasn’t selected anything yet, and so the response is basic:

{
  "01HTKMYZ0W7QT7VMSMMCFQNT1Q": {
    "vehicleMake": [ "Ford", "Toyota"],
    "vehicleModel": [],
    "vehicleYear": []
  }
}

The system has determined that the vehicleMake options are fully determined since they only depend on table data, and there are no where restrictions, so all values found in the table are valid from the beginning. In contrast, the vehicleModel selection does depend on the vehicleMake, and since there are no rows with an empty vehicleMake value, there are no matches for the inputs, and so there are no valid values returned.

Note

When there are no values returned for a particular field, the UI could be programmed to disable or hide those fields depending on the user experience they require.

Important

If a value is not included in the evaluation request, the system will defer to the currently persisted value of the element to fill those values. This behavior may change to treating those cases as having “no value” as described above.

After the user chooses a vehicle make, such as Ford, the UI can update the evaluation request as follows:

{
  "dependencyMap": { /* this hasn't changed from the earlier request */ },
  "01HTKMYZ0W7QT7VMSMMCFQNT1Q": {
    "vehicleMake": "Ford"
    "vehicleModel": {},
    "vehicleYear": {}
  }
}

And the response is:

{
  "vehicleMake": [ "Ford", "Toyota" ],
  "vehicleModel": [ "Mustang", "F150" ],
  "vehicleYear": []
}

Referencing Other Elements

When specifying the reference field that constrains another field, that reference field does not have to be in the same element. If only its name is included in the key property, it refers to that field on the same element. If prefixed with .., such as "../driverPoints", the constraint comes from the driverPoints field on the parent element. These can be chained; for example, ../../driverPoints refers to the driverPoints field on the parent’s parent element.

The root (product) element can be refered to by prefixing a slash, such as /driverPoints.

Validation

The discussion above refers to the up-front process used while the quote or policy transaction is still in draft state. When the user has made their selections, the transaction can proceed to validated state. By default, the system will apply the constraints implied by the configuration and constraint tables to ensure that the actual data meets the constraints, even if the origin of the data was not the UI.

Important

The ability to bypass this validation is not yet present, but will be added in an upcoming release.