config: the v2 state model
The workflow dropped four stages on 7 September and renamed two. This table is the console's mirror of it and was still describing the old shape, which breaks quietly rather than loudly. WHAT CHANGED IN THE WORKFLOW Risk Captured, Proposal Accepted, KYC Verified and Underwriting Cleared are gone. A lead entered and left each of them inside a minute, so they were pipeline steps wearing a stage's clothes. Meanwhile the one genuine stall -- a partner agent finding three documents -- had no stage at all and happened inside "Contacted", which meant the queue that most needed watching was the only one that did not exist. zk-state-docs is that queue, and it is where the work those four stages used to represent now happens: upload, capture, advise, quote, all without the lead moving. Quote Presented absorbs the other three the same way -- acceptance, identity and the underwriting screen -- and leaves in one move for Payment Pending, Referred or Declined, decided by a single gate that reads both answers. The two-gate chain it replaces could not express "identity refers but the risk is fine": the first gate routed to Referred before appetite had been asked, so an underwriter always received half a file. Qualified became Awaiting Contact and Quoted became Quote Presented. "Qualified" meant pre-call here and post-call on the whiteboard -- the same word on opposite sides of the dial. WHY THE RENAME IS THE DANGEROUS HALF STAGES.name is compared against current_state_name in three places: the queue filter, the sidebar tally, and the action lookup on the lead detail page. A workflow rename that is not mirrored here does not error anywhere -- the record view returns zero rows, correctly, for a name no lead is in. Both renamed queues would simply have read empty. The header comment now says so. Worse, zk-state-docs was absent from STATE_ACTIVITIES entirely, so a lead sitting there rendered no buttons at all. Document upload is the human step the entire rest of the flow hangs off. ALSO Parked / Nurture has been in the workflow since 4 September and was never added here, so its Resume Outreach button did not exist. Until a scheduler lands, that button is the only thing that wakes a parked lead, so it gets a nav group of its own rather than being filed under "running by itself", which it is not. DROPPABLE follows the new state list. Mark Lost now also reaches Referred, which the workflow had never allowed and now does: a lead whose cover lapses while an underwriter holds the file previously had nowhere to go. doc_vehicle_photos becomes conditional. The comment said no break_in flag existed to test -- one does now, derived at filing on all three doors, so the slot honours its own "break-in only" caveat instead of being shown to every motor renewal. Verified by diffing STAGES.name and ACTIONS against tbl_wf_states and tbl_wf_state_allowed_activities on dev; both match exactly.
This commit is contained in:
parent
66ea08e317
commit
b9c76482a2
@ -13,7 +13,7 @@ export const WORKFLOW = 'zk_wf_lead'
|
||||
*
|
||||
* Mirrors workflow.tbl_wf_states by hand. It is duplicated rather than fetched
|
||||
* because the sidebar has to render its ORDER, and the API returns states as a
|
||||
* set with no canonical sequence. Keep in step with 02_workflow.sql.
|
||||
* set with no canonical sequence. Keep in step with 53_v2_state_model.sql.
|
||||
*
|
||||
* `kind` answers "who is holding this lead", which is the question an operator
|
||||
* actually has — not "where is it in the process":
|
||||
@ -21,25 +21,36 @@ export const WORKFLOW = 'zk_wf_lead'
|
||||
* needs a person has to act, and nothing moves until they do
|
||||
* customer waiting on someone outside the business
|
||||
* auto an AI employee is carrying it; `doing` is what to say meanwhile
|
||||
* waiting real, but not yet — the renewal is too far off to work
|
||||
* end terminal
|
||||
*
|
||||
* `need` names the queue by what it wants done. A queue called "Upload
|
||||
* documents" answers "is anything waiting on me?"; one called "Contacted"
|
||||
* describes where the lead sits and leaves the operator to work it out.
|
||||
* documents" answers "is anything waiting on me?"; one called "Document
|
||||
* Pending" describes where the lead sits and leaves the operator to work it out.
|
||||
*
|
||||
* `name` MUST match workflow.tbl_wf_states.name exactly — the queue filter, the
|
||||
* sidebar tally and the action lookup all resolve a lead through this string
|
||||
* against current_state_name. A workflow rename not mirrored here does NOT
|
||||
* error: the queue returns nothing, correctly, for a name no lead is in. That
|
||||
* is the failure mode this table has, and it is silent.
|
||||
*/
|
||||
export const STAGES = [
|
||||
{ uid: 'zk-state-new', name: 'New Lead', kind: 'auto', doing: 'Qualifying…', by: 'Intake AI' },
|
||||
{ uid: 'zk-state-qualified', name: 'Qualified', kind: 'auto', doing: 'Calling the customer…', by: 'Voice agent' },
|
||||
{ uid: 'zk-state-contacted', name: 'Contacted', kind: 'needs', need: 'Upload documents', by: 'the partner agent' },
|
||||
{ uid: 'zk-state-risk', name: 'Risk Captured', kind: 'auto', doing: 'Pricing and advising…', by: 'Rating + Advisor AI' },
|
||||
{ uid: 'zk-state-quoted', name: 'Quoted', kind: 'customer', need: 'Accept the proposal', by: 'the customer' },
|
||||
{ uid: 'zk-state-accepted', name: 'Proposal Accepted', kind: 'auto', doing: 'Verifying identity…', by: 'KYC AI' },
|
||||
{ uid: 'zk-state-kyc', name: 'KYC Verified', kind: 'auto', doing: 'Screening for underwriting…', by: 'KYC AI' },
|
||||
{ uid: 'zk-state-new', name: 'New Lead', kind: 'auto', doing: 'Qualifying…', by: 'Intake AI' },
|
||||
{ uid: 'zk-state-qualified', name: 'Awaiting Contact', kind: 'auto', doing: 'Calling the customer…', by: 'Voice agent' },
|
||||
{ uid: 'zk-state-contacted', name: 'Contacted', kind: 'auto', doing: 'Writing the document request…', by: 'Engage AI' },
|
||||
// The principal stall in the workflow, and the one that had no stage until
|
||||
// 7 September: a lead waited for its three documents inside "Contacted", so
|
||||
// the queue that most needed watching was the one that did not exist.
|
||||
{ uid: 'zk-state-docs', name: 'Document Pending', kind: 'needs', need: 'Upload documents', by: 'the partner agent' },
|
||||
{ uid: 'zk-state-quoted', name: 'Quote Presented', kind: 'customer', need: 'Accept the proposal', by: 'the customer' },
|
||||
// The one queue where the machine stops and a person decides.
|
||||
{ uid: 'zk-state-referred', name: 'Referred to Underwriting', kind: 'needs', need: 'Clear or decline', by: 'an underwriter' },
|
||||
{ uid: 'zk-state-cleared', name: 'Underwriting Cleared', kind: 'auto', doing: 'Requesting premium…', by: 'Engage AI' },
|
||||
{ uid: 'zk-state-payment', name: 'Payment Pending', kind: 'needs', need: 'Confirm premium', by: 'ops' },
|
||||
{ uid: 'zk-state-issued', name: 'Policy Issued', kind: 'auto', doing: 'Closing the file…', by: 'Engage AI' },
|
||||
{ uid: 'zk-state-referred', name: 'Referred to Underwriting', kind: 'needs', need: 'Clear or decline', by: 'an underwriter' },
|
||||
{ uid: 'zk-state-payment', name: 'Payment Pending', kind: 'needs', need: 'Confirm premium', by: 'ops' },
|
||||
{ uid: 'zk-state-issued', name: 'Policy Issued', kind: 'auto', doing: 'Closing the file…', by: 'Engage AI' },
|
||||
// Nurture. There is no scheduler yet, so Resume Outreach is a button and it
|
||||
// is the only thing that wakes a parked lead — do not present this as a
|
||||
// stage with nothing to do.
|
||||
{ uid: 'zk-state-parked', name: 'Parked / Nurture', kind: 'waiting', need: 'Resume when the window opens', by: 'you, for now' },
|
||||
{ uid: 'zk-state-onboarded', name: 'Onboarded', kind: 'end' },
|
||||
{ uid: 'zk-state-lost', name: 'Lost / Dropped', kind: 'end' },
|
||||
{ uid: 'zk-state-declined', name: 'Declined', kind: 'end' },
|
||||
@ -54,6 +65,7 @@ export const NAV_GROUPS = [
|
||||
{ label: 'Needs someone', kind: 'needs' },
|
||||
{ label: 'With the customer', kind: 'customer' },
|
||||
{ label: 'Running by itself', kind: 'auto' },
|
||||
{ label: 'Not yet', kind: 'waiting' },
|
||||
{ label: 'Closed', kind: 'end' },
|
||||
]
|
||||
|
||||
@ -87,25 +99,42 @@ export const DV_LEAD = 'zk-dv-lead'
|
||||
const STATE_ACTIVITIES = {
|
||||
'zk-state-new': [{ uid: 'zk-act-qualify', label: 'Qualify Lead', by: 'Intake AI' }],
|
||||
'zk-state-qualified': [{ uid: 'zk-act-contact', label: 'Log Contact', by: 'Engage AI' }],
|
||||
'zk-state-contacted': [{ uid: 'zk-act-collect-docs', label: 'Collect Documents', by: 'Anyone — self-loop' },
|
||||
// Contacted has exactly one way forward and it belongs to Engage. An AI
|
||||
// cannot receive a file, so it writes the ask; the agent uploads one state
|
||||
// later. Giving the AI the activity that RECEIVES documents is how you get an
|
||||
// agent recording documents it never saw.
|
||||
'zk-state-contacted': [{ uid: 'zk-act-request-docs', label: 'Request Documents', by: 'Engage AI' }],
|
||||
// Four steps that used to be two stages: the upload, the capture, the advice
|
||||
// and the quote. The lead stays here for all of them and leaves only once a
|
||||
// premium exists.
|
||||
'zk-state-docs': [{ uid: 'zk-act-collect-docs', label: 'Collect Documents', by: 'Anyone — upload here' },
|
||||
{ uid: 'zk-act-capture-motor', label: 'Capture Motor Risk', by: 'Engage AI' },
|
||||
{ uid: 'zk-act-capture-sme', label: 'Capture SME Risk', by: 'Engage AI' },
|
||||
{ uid: 'zk-act-capture-motor', label: 'Capture Motor Risk', by: 'Engage AI' }],
|
||||
'zk-state-risk': [{ uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI' },
|
||||
{ uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI' },
|
||||
{ uid: 'zk-act-quote', label: 'Generate Quote', by: 'Rating engine' }],
|
||||
'zk-state-quoted': [{ uid: 'zk-act-accept', label: 'Accept Proposal', by: 'Customer' }],
|
||||
'zk-state-accepted': [{ uid: 'zk-act-collect-docs', label: 'Collect Documents', by: 'Anyone — self-loop' },
|
||||
{ uid: 'zk-act-kyc', label: 'Verify KYC', by: 'KYC AI' }],
|
||||
'zk-state-kyc': [{ uid: 'zk-act-uw-screen', label: 'Underwriting Screen', by: 'Rules' }],
|
||||
// Likewise absorbs acceptance, identity and the underwriting screen — none of
|
||||
// which was ever a stage a lead rested in. Generate Quote appears again as a
|
||||
// RE-quote for a customer who wants different cover; the lead does not move,
|
||||
// and quote_revisions counts them (0 on the first, 2 is the ceiling).
|
||||
'zk-state-quoted': [{ uid: 'zk-act-accept', label: 'Accept Proposal', by: 'Customer' },
|
||||
{ uid: 'zk-act-kyc', label: 'Verify KYC', by: 'KYC AI' },
|
||||
{ uid: 'zk-act-uw-screen', label: 'Underwriting Screen', by: 'KYC AI' },
|
||||
{ uid: 'zk-act-quote', label: 'Generate Quote (revise)', by: 'Engage AI' },
|
||||
{ uid: 'zk-act-collect-docs', label: 'Collect Documents', by: 'Anyone — self-loop' }],
|
||||
'zk-state-referred': [{ uid: 'zk-act-uw-prepare', label: 'Prepare Referral', by: 'Referral AI' },
|
||||
{ uid: 'zk-act-uw-clear', label: 'Clear Referral', by: 'Underwriter' },
|
||||
{ uid: 'zk-act-uw-decline', label: 'Decline Referral', by: 'Underwriter' }],
|
||||
'zk-state-cleared': [{ uid: 'zk-act-payment', label: 'Request Premium', by: 'Ops' }],
|
||||
// 'Payment webhook' was aspirational — there is no payment integration on the
|
||||
// platform and none is planned here. Collection happens on the insurer's own
|
||||
// rails; ops records the reference. See 35_reframe_to_what_exists.sql.
|
||||
'zk-state-payment': [{ uid: 'zk-act-realise', label: 'Confirm Premium Realisation', by: 'Ops' },
|
||||
{ uid: 'zk-act-nudge', label: 'Log Payment Nudge', by: 'Engage' }],
|
||||
'zk-state-issued': [{ uid: 'zk-act-onboard', label: 'Complete Onboarding', by: 'Ops' }],
|
||||
//
|
||||
// Request Premium moved here when Underwriting Cleared was removed: entering
|
||||
// this state IS the decision to collect, so the ask happens inside it.
|
||||
'zk-state-payment': [{ uid: 'zk-act-payment', label: 'Request Premium', by: 'Engage AI' },
|
||||
{ uid: 'zk-act-nudge', label: 'Log Payment Nudge', by: 'Engage AI' },
|
||||
{ uid: 'zk-act-realise', label: 'Confirm Premium Realisation', by: 'Ops' }],
|
||||
'zk-state-issued': [{ uid: 'zk-act-onboard', label: 'Complete Onboarding', by: 'Engage AI' }],
|
||||
'zk-state-parked': [{ uid: 'zk-act-resume', label: 'Resume Outreach', by: 'Whoever owns the book' }],
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,9 +149,8 @@ const STATE_ACTIVITIES = {
|
||||
const DROP = { uid: 'zk-act-drop', label: 'Mark Lost', by: 'Whoever holds the lead' }
|
||||
|
||||
const DROPPABLE = [
|
||||
'zk-state-new', 'zk-state-qualified', 'zk-state-contacted', 'zk-state-risk',
|
||||
'zk-state-quoted', 'zk-state-accepted', 'zk-state-kyc', 'zk-state-referred',
|
||||
'zk-state-cleared', 'zk-state-payment',
|
||||
'zk-state-new', 'zk-state-qualified', 'zk-state-contacted', 'zk-state-docs',
|
||||
'zk-state-quoted', 'zk-state-referred', 'zk-state-payment', 'zk-state-parked',
|
||||
]
|
||||
|
||||
export const ACTIONS = Object.fromEntries(
|
||||
@ -178,9 +206,10 @@ export const DOC_SLOTS = {
|
||||
// Motor.
|
||||
doc_rc: { line: 'motor' },
|
||||
doc_prev_policy: { line: 'motor' },
|
||||
// "Break-in only" per the policy, but no break_in flag exists on the instance
|
||||
// to test — the field name carries the caveat and the slot stays on motor.
|
||||
doc_vehicle_photos: { line: 'motor' },
|
||||
// "Break-in only" per the policy. The flag DOES exist now — 49 derives
|
||||
// break_in at filing on all three doors — so the slot can honour its own
|
||||
// caveat instead of being shown to every motor renewal.
|
||||
doc_vehicle_photos: { line: 'motor', when: (lead) => lead.break_in === 'yes' },
|
||||
|
||||
// SME.
|
||||
doc_gst_cert: { line: 'sme' },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user