We want to cede control of Pac-related post bind policy actions to folks outside of the engineering group, namely Product and Product Implementation. This will be controlled in two different places
- The product will control visibility of postbind policy actions and their functionality on a product-by-product basis.
- Launch darkly flags will control visibility of postbind policy actions and their functionality on a user-by-user basis.
Note: the underlying assumption of the following solution is that every policy action (and accompanying rulesets feature functionality) is considered “turned off” for everyone by default. From there, each action is “turned on” through a combination of product form <Meta>
tags and launch darkly flags.
A user will only be able to complete a particular post bind policy action IF:
- The action exists as a form in the product and is made explicitly available, AND
<FormMeta name="ActionAvailable" value="true" />
- The action has a corresponding launch darkly flag for its application context, AND
ipm-form-*
, i.e.ipm-form-discountsandsurcharges
- The action is “turned on” for a given user (based on a particular set of rules around the user in launch darkly)
Once a particular post bind policy action is available to a user, rulesets for that action will only be enabled IF:
- The ruleset is marked as "ready" in that particular product form AND,
<FormMeta name="RulesetsReady" value="true" />
- The ruleset has a launch darkly flag for its application context, AND
ipm-form-*-rulesets
, i.e.ipm-form-discountsandsurcharges-rulesets
- The ruleset is “turned on” for a given user (based on a particular set of rules around the user in launch darkly)
It's important to understand how Pac works today, as we're going to be moving some of this logic out of Pac.
Given a policy, you call Pac and pass in a bunch of information like who the user is, which application its being run in, and which action (product form) you want to run.
<Pac
policyActionId="discounts-and-surcharges"
applicationId="agentportal"
user="[email protected]"
{...otherProps}
/>
Under the hood, Pac does the following:
- Gets the product ID
/products?policyId=XXXX&postbind=true
[
{
"id":"fnic-ho3-sc-20171106:20171221-postbind",
"updated":1555459200000,
"archived":false,
"carrierId":"FNIC",
"carrierName":"Federated National Insurance Company",
"effectiveDate":1509926400000,
"renewalDate":1640908800000,
"product":"HOMP SC_Post-bind",
"alias":"HOMP SC_Post-bind",
"productType":"HOMP",
"productVersion":65
}
]
- Uses the product ID to get the forms for that product
/products/:product-id/forms
[
{
name: "scheduled-personal-property",
label: "Scheduled Personal Property",
metadata: {...},
model: {...},
groups: [...]
binds: [...]
}
{
name: "discounts-and-surchrages",
label: "Discounts and Surcharges",
metadata: {...},
model: {...},
groups: [...]
binds: [...]
}
{
name: "optional-coverages",
label: "Optional Coverages",
metadata: {...},
model: {...},
groups: [...]
binds: [...]
}
/* more here */
]
- Draw one of the returned product forms (the consuming app tells us which one based on a hard-coded set of available actions in Pac's README—soon this will just come from the server).
Each application will have to pass information about the logged-in user to Launch Darkly, which is the data we will use to run logic on for individual users. Today, in Agent Portal, it looks something like this:
{
key: toLower(username),
email: toLower(email),
firstName: toLower(fullname),
custom: {
roles: toUpper(roles),
alc: toUpper(alc),
affiliation: toUpper(affiliation),
channel: toUpper(channel),
isAdmin,
isBackoffice,
loginType,
organizationID: toLower(organizationID),
pupStatus
}
}
In Launch Darkly, we will setup rules on a user-by-user basis for each post bind policy action flag in each application.
Launch darkly will return to the client the list of ipm-
flags, indicating which post bind policy actions a particular user has access to (and of those actions, which ones have rulesets turned on). Example response from launch darkly:
{
"ipm-form-discountsandsurcharges":true,
"ipm-form-discountsandsurcharges-rulesets":true,
"ipm-form-optionalcoverages": false,
"ipm-form-optionalcoverages-rulesets": false,
"ipm-form-changeeffectivedate":true,
"ipm-form-changeeffectivedate-rulesets":true,
}
The client then needs a way to know what to do with these flags. This is where Pac will come in.
Pac will accept these flags from the parent application, and then ask the quoting API "hey, for product X, which forms are availble for use?" The quoting API will give Pac a bunch of info about the product forms, like each forms' name, label, and whether it's "ready" and rulesets are "ready". Pac will then compare the flags from launch darkly with the metadata in the product to determine which actions should be shown.
@TODO we'll need something like a "truncated" version of the /products/:id/forms
call. This means first getting the postbind product ID, then retrieiving the "truncated" version of what Pac will later ask for.
// Get the product ID
// QUOTING_API/products?policyId=XXXX&postbind=true
[
{
"id":"fnic-ho3-sc-20171106:20171221-postbind",
"updated":1555459200000,
"archived":false,
"carrierId":"FNIC",
"carrierName":"Federated National Insurance Company",
"effectiveDate":1509926400000,
"renewalDate":1640908800000,
"product":"HOMP SC_Post-bind",
"alias":"HOMP SC_Post-bind",
"productType":"HOMP",
"productVersion":65
}
]
// Then get the "truncated" version of the product form with the metadata controls
// QUOTING_API/products/:product-id/forms?compact=true
[
{
name: "discounts-and-surchrages",
label: "Discounts and Surcharges",
metadata: {
RulesetsAreReady: "true",
ActionIsReady: "true"
}
},
{
name: "optional-coverages",
label: "Optional Coverages",
metadata: {
RulesetsAreReady: "false",
ActionIsReady: "false"
}
}
]
As mentioned, Pac will diff the flags with the product info and return a data structure representing a list of post bind policy actions available to the current user in the current application’s environment. (Note: there might be actions that launch darkly says are available, but for a particular product are considered not ready. This allows the client to avoid drawing a button for an action that is not available.)
@TODO
Rather than have Pac call launch darkly directly, we'll export a function which should accept a couple parameters (like the ipm-
launch darkly flags) and will return a promise which resolves to a specific data structure after doing it's internal logic. The internal logic will ensure that the launch darkly flag for a specific product form is "on" (i.e. <Meta name="ActionIsReady">
and ipm-discountsandsurcharges: true
). Additionally, it will check if a launch darkly flag and the product both say rulesets are turned on (i.e. <Meta name="RulesetsAreReady" />
and ipm-discountsandsurcharges-rulesets
).
Example implementation in Pac:
/*
Pac will export a function which returns a promise and resolves to
a chunk of data representing what actions are available to a specific user
as well as the productId, which Pac will need when invoking a single function
{
productId: "...",
actions: [
{
name: "discountsandsurcharges",
label: "Discounts and Surhcarges",
enableRulesets: true
},
{
name: "optionalcoverages",
label: "Optional Coverages",
enableRulesets: false
}
}
}
*/
export function fetchPacActions({ launchDarklyIpmFlags, policyId }) {
let productId;
return fetch(`/products/?policyId=${policyId}&postbind=true"`)
.then(res => res.id)
.then(productId =>
fetch(`/products/${productId}/forms?truncated`)).then(actions => ({ productId, actions })
)
.then(({ productId, actions }) => ({
productId,
actions: actions
// exlcude any actions that don't have a flag in launch darkly and the product saying its ready
.filter(action => (
launchDarklyIpmFlags[`ipm-${action.name}`] && action.metadata.ActionIsReady
))
// return the data structure expected by the client consumer
.reduce((action, acc) => ([
...acc,
{
name: action.name,
label: action.label,
enableRulesets: action.metadata.RulesetsAreEnabled === "true"
}
]), [])
})
};
Through this promise Pac makes available, the client implementing Pac will have be able to get the info it needs to render a list of IPM actions available to the current user in the current application based on the current product. When any of those buttons are clicked, <Pac />
would get called to render that form. Example implementation:
import Pac, { fetchPacActions } from "pac";
class MyComponent extends React {
constructor() {
this.state = {
activePolicyActionId: "",
isLoading: true,
data: {} /* see above for example shape */
}
}
componentDidMount() {
const { policyId, allLaunchDarklyFlags } = this.props;
fetchPacActions({
policyId,
launchDarklyFlags: Object.keys(launchDarklyFlags).filter(key => key.startsWith("ipm-"))
}).then(data => {
this.setState({ data });
})
}
handleAction = (e) => {
this.setState({ activePolicyActionId: e.target.id });
}
render() {
const { activePolicyActionId, data: { actions }} = this.state;
const activeAction = activePolicyActionId
? actions.find(item => item.name === this.state.activePolicyActionId)
: undefined;
return (
<div className="container">
<button>Policy Actions</button>
<div class="dropdown">
{actions.length
? <img src="loading.gif" />
: <ul>
{actions.map(action =>
<li>
<button id={action.name} onClick={this.handleAction}>
{action.label}
</button>
</li>
)}
</ul>}
</div>
<div className="pac-form">
{activeAction
? <Pac
productId={this.state.data.productId}
policyActionId={activeAction.name}
policyActionTitle={activeAction.label}
enabledRulesets={activeAction.enableRulesets}
{/* other props here */}
/>
: "Choose an action to get started"}
</div>
</div>
}
}
When Pac gets called, we no longer have to fetch the product Id as we already have it. Now we just call the quoting API for the non truncated version of the product forms.
@QUESTION is there a case where launch darkly will say a form is available, and so will the product, and Pac won't be able to render it? not sure if we have to account for this...
- Ensure
<FormMeta>
tags are in ALL products - Decide on semantics and implement Launch Darkly flags for in each application and each application’s environment for: (AP today uses
ipm-form-${NAME}
so probably that same syntax, but it's worth noting that theNAME
should match the name of the form used in the product)- A product form being declared as "ready" for use, i.e.
ipm-form-discountsandsurcharges
- A product form being declared as "ready" for rulesets to be used,
ipm-form-discountsandsurcharges-rulesets
- A product form being declared as "ready" for use, i.e.
- Clearly define exactly what information LD needs about a user in order to run the kind of logic we want. Currently, we pass information about the user to LD in Agent Portal but we'll have to see about IPC Manager.
- Define exactly what kind of logic we'll need to run on a user (is what's in AP today good enough?) and implement that same thing in IPC Manager