Compare commits

..

3 Commits

Author SHA1 Message Date
8eb80dc371 console: the payment chase is the scheduler's, not a button
86 moved Log Payment Nudge onto a timer — booked 24h out by Request
Premium, re-booked at 48h to a cap of three — because it was firing 47
seconds after the request and recording an outcome for a conversation
nobody had had.

A scheduled activity has to carry ZERO roles or the scheduler's own
perform fails silently, so "Chase the payment" would now 403 for
everyone who pressed it. It stops being offered.

A person who actually wants to reach the customer has Reply to the
customer, which sends a WhatsApp rather than recording that one was due.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 14:59:12 +05:30
927d335280 console: show the WhatsApp exchange, and let ops answer through it
Engage can now answer a customer's question from the policy wordings
(82), so the lead file has a conversation in it rather than a single
recorded reply.

The audit trail renders both halves — "The customer said", then "We
replied" — so the exchange reads as one, which is what an operator
reviewing an acceptance nobody in the office made actually needs.

The lead file gets a Conversation group beside Proposal: the question,
the answer, and the count. Acceptance stays in Proposal, one tab away
from the words that produced it.

"Reply to the customer" is offered to ops_admin only. Engage declines
anything it cannot answer from the wordings, anything that is a
negotiation, and everything past three answers — so a person needs a way
to take over, and it has to be through the same thread or the reply
lands somewhere the lead file never sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 13:32:12 +05:30
e5b413ecac console: the customer accepts from their own phone
Recording the acceptance was the third human touchpoint, and it was a
person in the office typing what a customer had said somewhere else.
The customer's WhatsApp reply now drives it (81), so this stage has no
step of its own.

"Record the acceptance" drops from a step to a folded recovery lever —
kept, because a customer who says yes on a CALL still needs somebody to
record it, and because a reply Engage judged ambiguous has to be
actionable by a person.

That left Quote Presented with an empty screen at exactly the stage an
operator is most likely to wonder whether something has broken, so the
waiting state now says so. Grey, and no pulse: nothing is happening, and
that is the correct state — unlike the AI strip, where a still dot would
mean something is wrong.

The reply itself surfaces in two places: the audit trail carries it as
"The customer said", and the lead file files it under Proposal beside the
acceptance it produced. The evidence and the decision it justifies are
one line apart, which matters when nobody in the office made it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 13:26:46 +05:30
5 changed files with 59 additions and 4 deletions

View File

@ -42,7 +42,7 @@ export const STAGES = [
// 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: 'Document collection', by: 'the partner agent' },
{ uid: 'zk-state-quoted', name: 'Quote Presented', kind: 'customer', need: 'Customer decision', by: 'the customer' },
{ uid: 'zk-state-quoted', name: 'Quote Presented', kind: 'customer', need: 'Customer decision', by: 'the customer', doing: 'Waiting for the customer to reply on WhatsApp' },
// The one queue where the machine stops and a person decides.
{ uid: 'zk-state-referred', name: 'Referred to Underwriting', kind: 'needs', need: 'Underwriting referral', by: 'an underwriter' },
{ uid: 'zk-state-payment', name: 'Payment Pending', kind: 'needs', need: 'Premium confirmation', by: 'operations' },
@ -135,9 +135,15 @@ const STATE_ACTIVITIES = {
{ uid: 'zk-act-quote', label: 'Generate Quote', by: 'Rating engine', role: 'force' },
],
'zk-state-quoted': [
{ uid: 'zk-act-accept', label: 'Record the acceptance', by: 'you, on consent', role: 'do' },
// The customer accepts from their own phone: their WhatsApp reply is
// routed to Customer Reply, Engage reads it, and Engage performs this.
// Kept reachable — role 'force', folded away — because a customer who
// says yes on a CALL still needs somebody to record it, and because a
// reply Engage judged ambiguous has to be actionable by a person.
{ uid: 'zk-act-accept', label: 'Record the acceptance by hand', by: 'you, on consent', role: 'force' },
{ uid: 'zk-act-quote', label: 'Re-quote', by: 'Engage AI', role: 'again' },
{ uid: 'zk-act-collect-docs', label: 'Add a document', by: 'you', role: 'again' },
{ uid: 'zk-act-answer-customer', label: 'Reply to the customer', by: 'you, on WhatsApp', role: 'again' },
{ uid: 'zk-act-kyc', label: 'Verify KYC', by: 'KYC AI', role: 'force' },
{ uid: 'zk-act-uw-screen', label: 'Underwriting Screen', by: 'KYC AI', role: 'force' },
],
@ -148,7 +154,11 @@ const STATE_ACTIVITIES = {
],
'zk-state-payment': [
{ uid: 'zk-act-realise', label: 'Confirm premium received', by: 'you', role: 'do' },
{ uid: 'zk-act-nudge', label: 'Chase the payment', by: 'Engage AI', role: 'again' },
// Not offered: the chase is a SCHEDULED activity now (86), booked 24h
// out by Request Premium and re-booked at 48h to a cap of three. It
// carries zero roles so the scheduler can perform it at all, which means
// a button here would 403 for everyone. To actually reach the customer,
// use Reply to the customer — that sends something.
{ uid: 'zk-act-payment', label: 'Request Premium', by: 'Engage AI', role: 'force' },
],
'zk-state-issued': [

View File

@ -33,6 +33,11 @@ export const ACTIVITY_ROLES = {
'zk-act-advise': ['ai_advisor'],
'zk-act-quote': ['ops_admin', 'ai_engage'],
'zk-act-accept': ['ops_admin', 'partner_agent', 'bank_rm', 'csr_direct'],
// Ops only. The customer's questions are normally Engage's, and this is
// the way a person answers one Engage declined — through the same WhatsApp
// thread, so the reply lands on the lead file instead of somewhere nobody
// can find it later.
'zk-act-answer-customer': ['ops_admin'],
'zk-act-kyc': ['ops_admin', 'ai_kyc'],
'zk-act-uw-screen': ['ops_admin', 'ai_kyc'],
'zk-act-uw-prepare': ['ai_uw_referral'],
@ -44,7 +49,10 @@ export const ACTIVITY_ROLES = {
'zk-act-uw-decline': ['sme_underwriter'],
'zk-act-payment': ['ops_admin', 'ai_engage'],
'zk-act-nudge': ['ops_admin', 'ai_engage'],
// Zero roles in the workflow since 86 — the scheduler performs it, and a
// scheduled activity with any role list fails silently. Left here as an
// empty list so the mirror still records that the activity exists.
'zk-act-nudge': [],
'zk-act-realise': ['ops_admin'],
'zk-act-onboard': ['ops_admin', 'ai_engage'],
'zk-act-resume': ['ops_admin', 'partner_agent', 'bank_rm', 'csr_direct'],

View File

@ -37,6 +37,8 @@ const NARRATIVE = [
['referral_analysis', 'Referral analysis'],
['uw_decision_notes', 'Underwriting decision'],
['documents_notes', 'Documents'],
['customer_reply', 'The customer said'],
['customer_answer', 'We replied'],
['lost_reason', 'Why it was dropped'],
['resume_note', 'Why now'],
]

View File

@ -69,6 +69,7 @@ const GROUPS = [
['Motor risk', ['motor_reg_no','motor_make_model','motor_mfg_year','motor_cc','motor_fuel','motor_idv','motor_ncb_pct','motor_addons','motor_prev_insurer','motor_prev_policy_no','motor_prev_expiry','motor_prev_claim']],
['AI advice', ['ai_recommended_cover','ai_recommended_addons','ai_recommendation_rationale','ai_recommendation_confidence']],
['Quote', ['product_code','quoted_od_premium','quoted_tp_premium','quoted_addon_premium','quoted_section_premiums','quoted_gst','quoted_premium','quoted_breakup','quote_valid_till']],
['Conversation', ['customer_reply','customer_reply_from','customer_answer','answer_count']],
['Proposal', ['acceptance_ref','accepted_at']],
['KYC', ['kyc_mode','kyc_ref','kyc_outcome','kyc_mismatch_notes']],
['Underwriting', ['uw_outcome','uw_survey_required','referral_analysis','uw_referral_reason','uw_decision_notes']],
@ -368,6 +369,21 @@ export default function Lead() {
</button>
</div>
</div>
) : stage?.kind === 'customer' && stage.doing ? (
/* Not an agent working, and not a task of yours either the lead
is with someone outside the business. Without this the screen
went blank at exactly the stage where an operator is most likely
to wonder whether something has broken. */
<div className="doing doing--waiting">
<span className="doing__wait" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Their answer comes back to this lead on its own Engage reads it and
records the acceptance. Nothing is waiting on you.
</span>
</div>
</div>
) : stage?.kind === 'auto' ? (
<div className="doing">
<span className="doing__pulse" aria-hidden="true" />

View File

@ -1225,3 +1225,22 @@
color: var(--zk-muted);
margin-top: 2px;
}
/* ---- waiting on someone outside the business ----
Neither an agent working nor a task of yours. Grey rather than blue, and
no pulse: nothing is happening, and that is the correct state. */
.doing--waiting {
border-color: var(--zk-line);
background: var(--zk-tint);
}
.doing--waiting strong { color: var(--zk-ink); }
.doing__wait {
flex: none;
margin-top: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--zk-grey);
background: transparent;
}