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.
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.
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.
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.
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 viaParentMeasureID
. 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:
- String
- Money
- Float
- Boolean
- Int
- 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:
- String
- Int
- Int range
- Money
- Money range
- Date
- Date range
- Float,
- Float range,
- Boolean,
- List
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
, andBreach Days Delinquent
- Rows that are 'default centric' populate only
State
,UPB
,Loan Type
, andAgency
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
- inputs are
- 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
- inputs are
- Create a matrix called
Delinquent Chooser
(the master matrix)- inputs are
State
,UPB Range
, andDays delinquent
- output is
Matrix
: if a row is breach centric, the output isBreach Letter
; if the row is default centric, the output isDefault Workflow
- inputs are
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
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 aDimension
namedFoo
to the valueBar
-
- an XPath expression, if defined in
MatrixDimension.XPath
; the XPath expression is evaluated againstContact/Summary
-
Formatting:dateDiff('BirthDate', 'today', 'y')
would return the age of theContact
-
- a property of the object
-
Dimension=Status
would returnContact.Status
-
- the first XML node that matches the
Dimension
name-
Dimension=SomeUserDefinedField
would return the first task with a user defined field namedSomeUserDefinedField
-
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.
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.
- Create a Matrix named Loan Assignment
- Create an input called
UPBRange
, with a value type ofMoney Low - Money High
- Create outputs called
Status
andAssignedPersonID
- Add rows as you deem fit
- Call
Loan/UpdateFromMatrix?ID=X&FromMatrix=Loan Assignment&Overwrite=true
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