diff --git a/src/api/config.js b/src/api/config.js index 3fc4f76..b8bc58f 100644 --- a/src/api/config.js +++ b/src/api/config.js @@ -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' },