Rules API - quandis/qbo3-Documentation GitHub Wiki

The Matrix modules provides an inference-based rules engine. Rather than maintain a bunch of if/then or case statements in a procedural programming language, set up a Matrix to provide weighted recommendations.

Example

A client with several dozen service offerings tiers pricing by State, Client, Investor, and loan Balance range. In the table below, Product, State, Client, Investor and Balance are 'inputs' (Dimensions), while Cost and Price are 'outputs' (Measures):

State Client Investor Balance Cost Price
50 100
CA 60 120
Wells Fargo 50 90
Wells Fargo Goldman 50 95
>500K 50 110
TX <100K 45 100

The cost of a BPO is:

  • 45 in TX if the UPB < 100K, else
  • 60 in CA, else
  • 50 in all other cases

Pricing is more complicated:

  • 95 for Wells Fargo loans with Goldman as the investor, else
  • 90 for Wells Fargo loans, else
  • 110 for balances over 500K, else
  • 120 for CA, else
  • 100 in all other cases

Part of the challenge is determining which row wins if there are 'ties'. For example, should a BPO done in CA for Wells Fargo be priced at $90 or $120. The answer can be configured by applying weights to dimensions. For example, dimensional weights in this example are:

  • State: 5
  • Client: 10
  • Investor: 3
  • Balance: 1
Row State Client Investor Balance Cost Price Weight
1 50 100 0
2 CA 60 120 5
3 Wells Fargo 50 90 10
4 Wells Fargo Goldman 55 95 13
5 >500K 50 110 1
6 TX <100K 45 100 6

Note that row 3 has a weight of 10, beating row 2 with a weight of 5, and row 2 beats row 1 with a weight of 0. Rows 4 and 5 are not eligible, since they require criteria that the subject BPO does not contain.

Thinking Like a Business Analyst - Not a Programmer

A QBO Client tasked their IT department with creating a matrix to assess whether breach letters should not be sent on eligible loan. The rules were:

  • Don't send if more than 60 days delinquent, except for Investor ABC
  • Don't send if more than 45 days delinquent for Investor ABC
  • Don't send if more than 45 days delinquent, the state is AR, CA, FL, MA, NM, NY, RI, TX, WA, ME, and Hold is true
  • Don't send if the state is NJ, they don't have FC signing authority, and there is no investor
  • Don't send if more than 60 days delinquent and the state is PA

Their IT department create a 300 row matrix, covering all possible permutations of the rules; that is a row for every state, investor, and signing authority. Running against their entire breach-eligible loan portfolio took hours.

When their business unit reviewed the matrix, a business analyst redesigned the matrix to be 5 rows, with a row matching each bullet point listed in the requirements. Running against their entire breach-eligible loan portfolio took 6 minutes.

The Matrix module is intended to be configured in a manner that makes sense to a business analyst!

Programmers often tend to think in procedural manner, a mindset that frequently does not map "naturally" to business requirements. All too often, we let the inmates run the asylum. In this case, programmers genuinely did not think to use pattern matching and weights. Because they did not, their procedural trained minds looked at 5 bullet points of requirements, and came up with 300 lines of all possible permutations.

API

A rules matrix is intended to be configured and maintained by a business unit, but it is almost always consumed by API calls: either from other QBO modules, or from third party applications.

When calling Matrix/Lookup, you may pass zero or more inputs. The results returned will be the best weighted match given the inputs you passed.

POST: /Application/Matrix.ashx/Lookup?ID=1&State=CA
Response:
{
"MatrixCollection": {
"MatrixItem": [
{
  "RowNumber": "1",
  "FactCount": "1",
  "MatchCount": "1",
  "Weight": "5",
  "MisMatchCount": "0",
  "Cost": "60.00",
  "Price": "120.00",
  ...
},
{ ... }
] }

In this example above, we chose to pass just 1 input (State=CA), ignoring Client, Investor, and Balance. The best matching row was the CA row, with a cost of $60 and a price of $120. Regarding the data returned:

  • MatchCount: of all the inputs submitted, how many matched the row data?
  • MisMatchCount: of all the inputs submitted, how many conflicted with row data?
  • Weight: sum of the weight of matches inputs
  • FactCount: how many inputs were defined for the row?

Generally speaking, a consuming API should accept the first node returned, as long as it's MisMatchCount=0. If the best-matched row contains mismatches, that means your inputs did not successfully match ANY row in the matrix.

The sort order of the returned data is Weight, MatchCount.

Creating a matrix via API

If you wish to create a matrix via API calls (as opposed to consuming matrix rules via API calls like the example above), consider the following examples.

-- Create a Matrix
POST /Application/Matrix.ashx/Save?Matrix=Pricing&Output=Json
Response: {"MatrixID":1,"Matrix":"Pricing"}

-- Create outputs
POST: /Application/MatrixMeasure.ashx/Save?MatrixID=1&MatrixMeasure=Cost&ValueType=2&Value=50&Output=Json
Response: {"MatrixMeasureID":1,"MatrixMeasure":"Pricing","ValueType":2,"ValueMoney":50}
POST: /Application/MatrixMeasure.ashx/Save?MatrixID=1&MatrixMeasure=Price&ValueType=2&Value=100&ParentMeasureID=1&Output=Json
Response: {"MatrixMeasureID":2,"MatrixMeasure":"Pricing","ValueType":2,"ValueMoney":100}

Note: a Matrix must have at least one output column (MatrixMeasure). You may add more, but 'extra' columns must be bound to a primary output via ParentMeasureID. Think of the primary output as a row identifier; when adding additional outputs, you must specify which row they're associated with.

The MatrixMeasure.ValueType is enumerated as:

  1. String
  2. Money
  3. Float
  4. Boolean
  5. Int
  6. Object/ID
-- Create input columns
POST: /Application/MatrixDimension.ashx/Save?MatrixID=1&MatrixDimension=State&ValueType=10&Output=Json
Response: {"MatrixDimensionID":1,"MatrixDimension":"State","ValueType":10}
POST: /Application/MatrixDimension.ashx/Save?MatrixID=1&MatrixDimension=Client&ValueType=1&Output=Json
Response: {"MatrixDimensionID":2,"MatrixDimension":"Client","ValueType":1}
POST: /Application/MatrixDimension.ashx/Save?MatrixID=1&MatrixDimension=Investor&ValueType=1&Output=Json
Response: {"MatrixDimensionID":3,"MatrixDimension":"Investor","ValueType":1}
POST: /Application/MatrixDimension.ashx/Save?MatrixID=1&MatrixDimension=Client&ValueType=4&Output=Json
Response: {"MatrixDimensionID":2,"MatrixDimension":"Balance","ValueType":4}

The MatrixDimension.ValueType is enumerated as:

  1. String
  2. Int
  3. Int range
  4. Money
  5. Money range
  6. Date
  7. Date range
  8. Float,
  9. Float range,
  10. Boolean,
  11. List

Nested Matricies

Some matrices may become very complex, particularly if they're being used to field requirements from multiple business units. For example:

  • In some states, loan characteristics (UPB, days delinquent) may require creation of a breach letter
  • Other states may require launching of a default workflow
  • Breach letter templates may be based upon investor, state, and other criteria
  • Default workflow templates may be based upon additional loan characteristics (UPB, loan type, agency backing)

Creating a single matrix to field all these requirements gets a bit unwieldy:

  • Dimensions: State, Breach UPB Range, Breach Days Delinquent, Investor, Default UPB Range, Loan Type, Agency
  • Rows that are 'breach centric' populate only State, Breach UPB Range, Investor, and Breach Days Delinquent
  • Rows that are 'default centric' populate only State, UPB, Loan Type, and Agency

In such cases, you may instead configure a nested matrix: a matrix that calls other matrices. For the example above:

  • Create a matrix called Breach Letter
    • inputs are State, UPB Range, Investor, Days Delinquent
    • output is an Attachment Template that determines which beach letter to launch
  • Create a matrix called Default Workflow
    • inputs are State, UPB Range, Loan Type, Agency
    • output is a DecisionTemplate that determines which default workflow to launch
  • Create a matrix called Delinquent Chooser (the master matrix)
    • inputs are State, UPB Range, and Days delinquent
    • output is Matrix: if a row is breach centric, the output is Breach Letter; if the row is default centric, the output is Default Workflow

This allows the breach letter power users to maintain the breach matrix independently of the default workflow power users.

When calling Matrix/Lookup?ID={Delinquent Choose}&State=CA&UPB=127522.78&DaysDelinquent=35, assuming this matches a breach row, the output will include:

  • Matrix: Breach Letter
  • Attachment Template: {Some Attachment Template}
    • note there are no 'default workflow' outputs here to clutter things up

When calling Matrix/Lookup?ID={Delinquent Choose}&State=NY&UPB=127522.78&DaysDelinquent=62, assuming this matches a default row, the output will include:

  • Matrix: Default Workflow
  • Decision Template: Some Decision Template
    • note there are no 'breach letter' outputs here to clutter things up

To leverage nested matrices, the output of the master matrix must:

  • be a string output type,
  • include the word 'Matrix' in the output name, and
  • its value must be the name of an existing matrix

Binding Objects to Matricies

The Matrix module supports a LookupByObject method that will gather the Dimension data from an AbstractObject. In this method, the data for a Dimension will bind from:

  • a parameter, if present
    • Matrix/LookupByObject?ID=X&Object=Contact&ObjectID=Y&Foo=Bar will bind a Dimension named Foo to the value Bar
  • an XPath expression, if defined in MatrixDimension.XPath; the XPath expression is evaluated against Contact/Summary
    • Formatting:dateDiff('BirthDate', 'today', 'y') would return the age of the Contact
  • a property of the object
    • Dimension=Status would return Contact.Status
  • the first XML node that matches the Dimension name
    • Dimension=SomeUserDefinedField would return the first task with a user defined field named SomeUserDefinedField

XPath Expression Examples

The MatrixDimension.XPath property enable mapping to custom functionality:

Example XPath Description
Term digit substring({//Loan}, string-length({//Loan})) Extract the last digit of a loan number (or any other field).
Last item //MyCustomField[last()] If a property may occur repeatedly, this will fetch the last instance (generally by CreatedDate).
Dates Formatting:dateDiff({//CreatedDate}, 'today', 'd') Calculates the number of days between a CreatedDate and today.
API Call Data:invoke('Contact', 'MyCustomMethod', 'Foo={//Bar}')//FirstName Invokes a method signature (Contact/MyCustomMethod?Foo=Bar), and extract a FirstName note from the results. Note the method signature must return an XmlReader in this case.

Please note the location of curly braces. If you have an XPath selector within the XPath function, you must wrap the XPath within curly braces. This tells the substitution method to fill in these values from the Summary data before running the XPath function. Then, to retrieve an element of the result of the XPath function, include another selector immediately after the function, such as in the API Call example above.

Updating Objects From a Matrix

The AbstractObject/UpdateFromMatrix method can be used to change the properties of any QBO object by evaluating it against a Matrix. Examples include:

  • Set a Loan.AssignedPersonID based on a Matrix output
  • Set an Organization.Status based on a Matrix output

To leverage this feature, create a Matrix with:

  • 1 or more inputs (Dimensions) that consider data associated with an object
  • 1 or more outputs (Measures) that exactly match the field names of an object
  • Invoke {ClassName}/UpdateFromMatrix?ID={ID List}&FromMatrix={Matrix}

Parameters:

  • ID (Int64): one or more identity values for the objects being updated.
  • Overwrite (boolean)
    • When true, any Matrix outputs will overwrite the Object's values
    • When false, any Matrix outputs will only overwrite the Object's values if the Object's values are NULL

As with all standard methods, this can be used from a workflow step.

Loan Assignment Example

  • Create a Matrix named Loan Assignment
  • Create an input called UPBRange, with a value type of Money Low - Money High
  • Create outputs called Status and AssignedPersonID
  • Add rows as you deem fit
  • Call Loan/UpdateFromMatrix?ID=X&FromMatrix=Loan Assignment&Overwrite=true

Bulk Lookup

Matrix/BulkLookup supports batch lookup operations. Assume we have a Matrix called Required Docs like this:

State AppType Package
HAMP Loan Mod HAMP Package
CA HAMP Loan Mod HAMP Package - CA
Liquidation Closeout
MA Liquidation MA Closeout

One can call Matrix.ashx/BulkLookup?Matrix=Required Docs passing a payload like this:

<Batch>
    <Input>
        <Loan>123</Loan>
        <AppType>HAMP Loan Mod</AppType>
        <PropertyState>WA</PropertyState>
    </Input>
    <Input>
        <Loan>234</Loan>
        <AppType>Liquidation</AppType>
        <PropertyState>MA</PropertyType>
    </Input>
</Batch>

and you will get back something like this:

<MatrixBulkLookup>
    <Output>
        <Loan>123</Loan>
        <AppType>HAMP Loan Mod</AppType>
        <PropertyState>WA</PropertyState>
        <Package>HAMP Package</Package>
    </Output>
    <Output>
        <Loan>234</Loan>
        <AppType>Liquidation</AppType>
        <PropertyState>MA</PropertyType>
        <Package>MA Closeout</Package>
    </Output>
</MatrixBulkLookup>

Note the Loan node in there for human eyes; it will be regurgitated in the output, but is useless to the Matrix unless you have a Loan dimension.

The reasons for using BulkLookup include:

  • You get back only the 'best match', Matrix/Lookup is giving you all matches (more effort, more data over the wire)
  • Significant reduction in HTTP connection chatter vs calling Lookup for each input node

One may also make the call asynchronous:

Application/Matrix.ashx/AsyncLookup?Matrix=Required Docs

This will save the XML input payload to imaging, and queue the job for processing asynchronously. The output payload will be saved to imaging, and made available for retrieval.

A call to Matrix/AsyncLookup will return an ImportFile XML node:

<ImportFileItem xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
 <Object>Matrix</Object>
 <ObjectID>225</ObjectID>
 <ImportFile>Matrix Async Lookup</ImportFile>
 <ImportFileID>5035</ImportFileID>
 <ImportFileTemplateID>36</ImportFileTemplateID>
 <Items />
 <PreTransformID>117557</PreTransformID>
</ImportFileItem>

You can then poll for results via:

Import/ImportFile.ashx/Select?ID={ImportFileID}&Output=Xml

looking for a Status node of 'Complete':

<ImportFileItem xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
 <Status>Complete</Status>
 <Object>Matrix</Object>
 <ObjectID>225</ObjectID>
 <ImportComplete>2014-06-17T11:46:35.077</ImportComplete>
 <ImportFile>Matrix Async Lookup</ImportFile>
 <ImportFileID>5035</ImportFileID>
 <ImportFileTemplateID>36</ImportFileTemplateID>
 <ImportResultID>117558</ImportResultID>
 <ImportStart>2014-06-17T11:43:02.183</ImportStart>
 <Items />
 <PreTransformID>117557</PreTransformID>
</ImportFileItem>

From here, you can download the results from the document stored as the ImportResultID:

Attachment/Download?ID=117558 
⚠️ **GitHub.com Fallback** ⚠️