Repository navigation
fix(lead): a rep's own lead no longer tells them it awaits assignment (#2073 item 7) - #2195
Conversation
lead_assignment alerts the lead's owner, but crm.lead_routing_new read
"New lead to assign: ... is awaiting assignment" and crm.lead_routing_hot
"Hot lead - assign within 24h ... needs an owner today": an assigner's
message sent to the person the lead was already assigned to. A write that
carries a user is stamped with that user as owner, so every lead a rep
created, from the form or through the API, sent that rep the message.
The standard gate e10 now also asks who put the lead in the owner's name:
it alerts only when created_by is blank, absent or a different person
(the round-robin's pick on web-to-lead or a user-less write, an
integration's named owner, a colleague logging it for them). The hot gate
e8 is unchanged on purpose: the hot alert is the 24h follow-up SLA and the
rating may be the system's own score. Both templates now address the
owner in all four locales ("New lead assigned to you", "Hot lead - follow
up within 24h"). Nobody who assigns leads is a messaging audience, so no
"awaiting assignment" alert is sent at all.
Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
席位核对ACCEPT (#2073 item 7) · PR #2195 at
Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing Generated by Claude Code |
…ice-owned-lead Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…budget The partly refused Enroll Members case ran past vitest's 5 s default on CI (Build and Test, 22.x). An Enroll Members run that writes members enrolls every eligible lead the caller can see, and that set grows with each world() in the file. Measured on head and on base alike: about 2 s alone, about 4.3 s beside two other booted files under coverage, as CI runs the suite. The lead_assignment change on this branch does not run in this file (world() seeds its leads, and a seed fires no record-change flow). The two clean runs (it.each timeout argument) and the partly refused describe (describe option) get 30 s, the budget the escalation-task suite uses. The global testTimeout is unchanged. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
席位核对 — patch roundACCEPT (patch round) · PR #2195 at This note supersedes the body's "Gates (on 851512d)" section for the current head. The body is left as written.
Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing |
Part of #2073 (item 7)
Clause-②: no
A lead a rep creates for themselves no longer sends that rep "New lead to assign: … is awaiting assignment". This PR is item 7 of the card only. Items 5, 6 (its second half), 8 and 10 remain open there.
What was wrong (measured on
e6d5d3e9)lead_assignmentalerts the lead's owner, but both of its templates were written for the person who assigns leads:crm.lead_routing_newread "New lead to assign: … is awaiting assignment";crm.lead_routing_hotread "Hot lead — assign within 24h: … needs an owner today".A write that carries a user is stamped with that user as owner before the flow runs. So for every lead a rep types in, the owner and the creator are the same person.
Measured on the real engine (
@objectstack/verifyboot of the shipped artifact,@objectstack/*18.0.0-next.5). One lead per door, and everylead_routingdelivery about it read:POST /data/crm_leadwithowner_id= rep (the create form's shape)POST /data/crm_leadwith no owner (API)POST /forms/contact-us/submitowner_id= repowner_id= repThe quoted titles are the rendered
sys_inbox_message.titlerows, read after the dispatcher had drained.The premise holds, and assumptions 1 and 2 are confirmed. One refinement: the notify nodes already skip an ownerless lead (#2060 item 6). So every one of these messages went to the lead's own owner, never to a lead awaiting assignment.
Browser, 1440×900.
objectstack devran on this worktree, with thepnpm demo:staffpersonas:--compile): na.rep creates a lead through Leads › New and leaves Lead Owner empty. The bell then shows "New lead to assign: Ada Form before … · … is awaiting assignment." An anonymous web lead lands on eu.rep, and eu.rep's bell shows "New lead to assign: Ada Web before …".The change
src/sales/flows/lead-assignment.flow.ts. The standard owner gatee10now also asks who put the lead in the owner's name. It goes on tonotify_stdonly whencreated_byis absent, blank, or a different person fromowner_id.has()guards both keys, andisBlankcovers null.e11("No owner, or the owner wrote it") ends the run after the SLA stamp, as it does for an ownerless lead.e8is unchanged on purpose (below).src/sales/email-templates/{en,zh-CN,es-ES,ja-JP}.ts. Both templates now address the owner:crm.lead_routing_new: "New lead assigned to you: …" · "… is now yours. Follow up within 3 days."crm.lead_routing_hot: "Hot lead — follow up within 24h: …" · "… (rating N) is yours. Follow up by tomorrow."The template names and labels are unchanged. The wording ships in the artifact, so it reaches an existing install on upgrade. Measured on one database: a boot compiled from base rendered the old text, and a boot built from this head rendered the new text.
The recipient: whoever assigns leads is not a messaging audience
Assumption 3, measured:
user:,role:,team:andowner_of:, and has noposition:spelling (@objectstack/service-messaging18.0.0-next.5, theRecipientResolvernote onsys_notification_subscription).role:there means organization membership (owner, admin, delegated admin, member), not a sales rung.src/addresses anyone but a record owner or the current user.content/docs/administration/setup.mdx§8 says there is no lead queue: thesales_reppool, filled by the round-robin, is the routing table.The people who assign leads by hand are defined by a permission (
sales_managercarriesallowTransferoncrm_lead), not by an audience. Telling them would take a new team or a new holder lookup, and the dispatch rules that out ("do not invent a new role or setting").So this PR sends no "awaiting assignment" message at all. The acceptance allows that ("If one is produced …"). The alert on an owned lead is now the owner's follow-up notice.
The hot path (assumption 4)
Same wording defect, measured. A rep's own hot lead sent that rep, by inbox and email, "Hot lead — assign within 24h … needs an owner today".
Not the same recipient defect. Every doc that describes this alert calls it the owner's follow-up SLA alert:
src/docs/crm_sales.md: "The owner is alerted to follow up within 24h";content/docs/sales/leads.mdx: "Alert to the lead's owner";"Hot" is also the system's verdict:
lead_automationscores a lead created without a rating. So the hot template is reworded and its recipient kept: a rep's own hot lead still alerts the rep.The asymmetry is deliberate and written down twice: in the flow header and the
e10comment, and in the pin "a rep's own HOT lead still alerts the rep".Route change from the suggested route
Zone 3 suggested sending the "awaiting assignment" notice only for a lead with no human owner after assignment ran. That would leave every owned lead with no alert at all, including a web lead the round-robin has just given a rep.
setup.mdx§8 documents that alert ("an alert to the new owner"). Instead, the notice becomes the owner's notice, sent when someone else assigned the lead.Options weighed (four axes)
setup.mdx§8.created_by) that every write door sets.Tests
test/flow-record-change.test.tshas a new describe, lead_assignment — who the routing alert reaches (#2073 item 7), with one pin per shape. Each pin reads everylead_routingdelivery about its lead, to anyone, on any channel. The empty answers are read only after the run has landed and passednotify_stdover.[{rep, inbox, crm.lead_routing_new}].[{rep, email, crm.lead_routing_hot}, {rep, inbox, crm.lead_routing_hot}].Other test changes:
hot-lead SLA routingdescribe now has the administrator write its leads in the rep's name, because a rep's own standard lead alerts nobody.test/hot-lead-threshold-parity.test.tsquotes the new hot subject in its header and failure text.Ablation, run once and not kept. Tool: objectstack
scripts/ablation-replace.mjs, wrap mode, on committed851512df. Mutation: delete the creator clause frome10(anchor 1 → 0, blobbc27a430→20d001af).Tests 2 failed | 52 passed (54). Exactly pins 1 and 2 failed, as predicted:notify_std was not passed over: expected 'success' to be 'skipped'.bc27a430= HEAD, andgit diff HEADis empty.objectstack.config.tsfrom source.Gates (on
851512df)Every run went through
os-verify-lock, and each result was read from its VERDICT line.pnpm validate && pnpm typecheck && pnpm lint && pnpm lint:i18n-gate && pnpm hygiene && pnpm hygiene:tokens && pnpm build→VERDICT command-exit 0.lead_assignment.i18n/missing-*issues; source hygiene clean.dist/objectstack.jsoncarries the newe10source.pnpm testasvitest run --shard=k/3 --maxWorkers=2, each shardVERDICT command-exit 0.find test -name '*.test.ts'.Docs
content/docs/sales/leads{,.zh-Hans,.zh-Hant}.mdx:content/docs/administration/automation{,.zh-Hans,.zh-Hant}.mdx: the New Lead Routing & SLA row says a standard lead alerts its owner only when someone else assigned it.setup.mdx§8: "an alert to the new owner", which is about round-robin intake.src/docs/crm_sales.md: the hot owner alert and the standard deadline.Acceptance notes
src/sales/views/lead.view.ts:427, a comment onhot_leads, still quotes the old subject "Hot lead — assign within 24h". It is outside the claim's file surface, so it is untouched. It is a comment, not product text.src/sales/mappings/lead_import.mapping.tsrecords that a person's import stamps the importer as owner. By the samecreated_by=owner_idreading, the importer gets no standard alert, and a row naming another owner alerts that owner. This is inferred from the measured API shape.POST /api/v1/data/crm_leadanswers 201, and the main frame is then replaced byabout:blankwith no navigation request. The lead is stored. This is unrelated to this diff, the console component that does it is not located, and a headed browser was not tried.Generated by Claude Code