Skip to content

fix(lead): a refused lead conversion ends refused instead of toasting success - #2114

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-2060-item-2c-refusal-outcome
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-2060-item-2c-refusal-outcome

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #2060 (item 2c only, comment 6181884832). The card stays open for its other items.
Clause-②: no

What was wrong

Convert Lead refuses three kinds of lead before its form opens:

  • a lead whose Duplicate Status is Confirmed (refuse_confirmed_duplicate);
  • a lead whose Conversion Approval is Pending or Rejected (refuse_unapproved);
  • a lead whose company has no match key (refuse_no_match_key).

Each refusal was a field-less waitForInput screen that resumed into the plain end (e26 / e30 / e33). So the dialog offered Submit, and Submit completed the run. The console then toasted convert_lead's "Lead converted successfully!" over a lead it had not converted.

Measured on base abc16634 (@objectstack/* 18.0.0-next.1). The server was objectstack dev -p 4162 --fresh, driven as the dev admin by Playwright on /opt/pw-browsers/chromium, read with DOM probes:

path dialog buttons toast after Submit lead after accounts created
refuse_confirmed_duplicate Cancel / Submit / Close "Lead converted successfully!" qualified, not converted 0
refuse_unapproved (Rejected; the stale-page path, see Acceptance notes) Cancel / Submit / Close "Lead converted successfully!" qualified, not converted 0
refuse_no_match_key (match key NULL) Cancel / Submit / Close "Lead converted successfully!" qualified, not converted 0

At the HTTP door, POST /api/v1/automation/lead_conversion/trigger answered 200 on all three, with status: paused, a runId, and the refusal as the screen.

The change

  • The three nodes are refusing end nodes. Each is type: 'end' with outcome: 'refused' and a message (spec EndConfigSchema). This is the shape refuse_nonpositive_amount (item 2b) and quote_generation's refuse_held already use. The run ends refused, is never resumed, and the console shows the message with Close only. When each one refuses is unchanged: the decision edges into them (e25, e28, e32) are untouched.
  • Same words, one slot. An end node has no heading: the spec's guidance is that "a runner shows message alone under the flow's label". Each message is therefore the screen's title, a sentence break, then its description, for example "Conversion refused. This lead's Duplicate Status is Confirmed: …". No word is added, dropped or changed. The console titles the dialog with the action's label (Convert Lead), so without the joined title the words "Conversion refused" would have disappeared from the dialog.
  • Edges. e26, e30 and e33 are retired, and their ids stay vacant under the file's edge-id rule. A refusing end has no outgoing edge.
  • Translations. In zh-CN, es-ES and ja-JP, each refusal moved from flows.lead_conversion.screens.NODE.title / .description to flows.lead_conversion.refusals.NODE.message. Title and description are joined the same way: 。 in zh-CN and ja-JP, a period in es-ES. No screens.refuse_* key remains in any pack.
  • One comment that became false. refuse_nonpositive_amount's comment said it was "not the message-only screen the three refusals above use". It is reworded. Its config and edges are untouched, and so is decision_amount.
  • A changeset for the release notes.

Measured after the change

These readings are from head e0ecf3c8, built with pnpm build and served by objectstack dev -p 4162 --fresh.

  • Console, en-US browser: I clicked Convert Lead on each path.
    • The dialog reads Convert Lead / Conversion refused. … (the full message), and its buttons are ["Close","Close"] (the corner X and the footer button).
    • No toast fired while the dialog was open, or after Close.
    • The lead stayed qualified and unconverted, and 0 accounts were created, on all three paths.
  • Console, zh-CN browser: the same on all three paths. The dialog reads 转化线索 / 转化被拒绝。…, the buttons are ["关闭","关闭"], there is no toast, and nothing is written.
  • HTTP door: I triggered all three leads with Accept-Language set to en-US, es-ES, ja-JP and zh-CN, 12 runs in all.
    • Every run answered 200 with status: refused. None carried a runId, a screen or a successMessage.
    • refusalMessage came back in the requested language, opening Conversion refused. / Conversión rechazada. / 変換は拒否されました。 / 转化被拒绝。.
    • After all 12 runs, the leads were still qualified and unconverted, with 0 accounts.
  • Control: a clean lead still opens screen_1. Resuming it with an amount of 25000 converted it, with an account and an opportunity linked.

Tests

The three suites that pinned the screens now pin the refused outcome and the store in one object:

  • success: true, status: 'refused', runId undefined, no screen;
  • nothing written and the lead unconverted;
  • a non-empty refusal message.

The changes by file:

  • test/lead-duplicate-visibility.test.ts: the three confirmed-survivor cases now pin that outcome. The "says why" case reads refusalMessage and keeps every earlier wording assertion: "refused", "Duplicate Status is Confirmed", "reviewer", the Duplicate Management group label, "surviving record", no record id, and no override hatch.
  • test/account-name-normalized-match.test.ts: the key-less lead is refused. The earlier store assertions are unchanged.
  • test/lead-conversion-approval-gate.test.ts: the Pending and Rejected cases keep the static check that e28 targets refuse_unapproved, and now assert that no edge leaves it. Each case also runs the conversion as the rep and pins the refused outcome: 0 accounts, 0 contacts, lead qualified and not converted.
  • test/flow-conversion.test.ts: one comment that said item 2b's was the only refusal ending refused.

Readings:

  • Premise, the new assertions against the unchanged flow and packs: Tests 7 failed | 150 passed (157). Each refusal came back paused, with a runId and the refusal screen. The approval-gate cases went red on their static half (e30 leaves refuse_unapproved).
  • A0 counter-proof, on committed e0ecf3c8: the base flow and the three base packs were written on disk, with the blobs checked. The result was Tests 7 failed | 150 passed (157), the same shape.
  • A1 counter-proof, on committed e0ecf3c8: only refuse_unapproved lost its outcome and message, which makes it a refusal that completes. Its three pack entries were removed too, using ablation-replace with --absent verified. The result was Tests 2 failed | 155 passed (157): the approval-gate runtime half went red on status while its static half held.
    • My first A1 attempt was refused by the tool, because its replacement text already occurred in the file. Nothing was measured that time, and the tool restored the file.
  • Restores: each counter-proof restored the files with git checkout HEAD, proven by blob == HEAD for all four files and an empty git diff HEAD.
  • Targeted set, 19 files: every suite that names the flow, plus i18n-references, lint-i18n-gate, flow-condition-totality, flow-decision-authority and flow-quote. Result: Tests 659 passed (659).

Local gates on e0ecf3c8, each exit code captured on its own:

  • pnpm validate 0 (✓ Validation passed)
  • pnpm typecheck 0
  • pnpm lint 0 (18 suggestion(s), 0 errors)
  • pnpm lint:i18n-gate 0 (✓ 0 i18n/missing-* issues)
  • pnpm hygiene 0 (✓ source hygiene clean)
  • pnpm hygiene:tokens 0 (✓ source token ratchet clean; src/sales business semantics ~57,926 of ~88,500)
  • pnpm build 0 (✓ Build complete)
  • pnpm test, run under the verify lock: VERDICT command-exit 0, Test Files 180 passed (180), Tests 3679 passed | 1 skipped (3680)

Playwright e2e is left to CI.

Acceptance notes

  • Reaching refuse_unapproved in the console. convert_lead's visible predicate hides the button on a Pending or Rejected lead, so in the console this refusal is reached only from a stale page: the verdict changes while the rep has the lead open. I measured that path by writing rejected as the system after the page loaded. A REST caller or the AI tool reaches it directly. This is by design, not a defect.
  • The dialog title changed. The refusal dialog is now titled with the action's label (Convert Lead), where the paused screens showed the flow's label (Lead Conversion Process). That is the console's choice for a refused run, the same as refuse_held.
  • Docs untouched. No sentence in content/docs/sales/leads*.mdx, lead-approval*.mdx or docs/MAINTENANCE.md became false. Convert still opens a refusal that says "Conversion refused" instead of the conversion form.
  • No platform producer is involved. The refusal uses declared constructs only.
  • Commit trailers are the model-free pair.

Generated by Claude Code

The three conversion refusals (a confirmed duplicate, a conversion approval
that is pending or rejected, a company with no match key) were message-only
waitForInput screens that resumed into the plain end. Submit on the dialog
therefore completed the run, and the console toasted convert_lead's
"Lead converted successfully!" over a lead that had not been converted
(measured in the console on all three paths).

Each refusal is now an end node with outcome 'refused' and a message, the
shape refuse_nonpositive_amount and quote_generation's refuse_held use: the
run ends refused, is never resumed, and the console shows the message with
Close only. An end node has no heading, so each message opens with the
words the screen's title carried ("Conversion refused") and the body is
unchanged. The edges e26 / e30 / e33 (refusal to end) are retired and their
ids stay vacant. The translations move from the screens' title and
description to refusals.NODE.message in zh-CN, es-ES and ja-JP, same text.

The suites that pinned the three screens now pin the refused outcome and an
empty store together.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
@vercel

vercel Bot commented Oct 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 15, 2026 1:49am UTC

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Oct 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

复核人:repo:hotcrm 执行席(#1353)· 判决 ACCEPT · 钉 head e0ecf3c83b1b6c67186f6e1056b1e36033f807c8 · 2026-10-15T01:56Z

形态:Part of #2060(第 2c 项),无关闭关键词,Clause-②: no;9 文件,+143/−128 = 271 行(派发预算约 200;净增 +15,多出的是删除的屏幕配置、退役的三条边和随之失真的注释);路径面无 governed 面 → 非 Tier H,席位落地。origin/main abc16634 是 head 的祖先(自派发后 main 未动)。

前提实测:diff 逐行读过。refuse_confirmed_duplicate / refuse_unapproved / refuse_no_match_key 由 waitForInput 屏改为 type: 'end' + outcome: 'refused' + message,与第 2b 项的 refuse_nonpositive_amount 同形;进入它们的判定边(e25 / e28 / e32)不动;e26 / e30 / e33 退役,id 留空并登记进文件的空号说明。文案:英文 message = 原标题 "Conversion refused." + 原描述,一字未改;三个语言包把同一段(标题 + 句号 + 原描述)从 screens.*.{title,description} 移到 refusals.*.message,无残留键(抽读 zh-CN 三段逐字对过)。dev 读数:基线上三条路径在控制台都弹 Cancel / Submit / Close,点 Submit 提示「Lead converted successfully!」而线索未转化;分支上三条路径都只显示拒绝文案和「关闭」、无提示、零写入(en-US、zh-CN),HTTP 门 4 种语言均返回 refused 与对应语言的 refusalMessage。切除 A0(回到基线)红 7 例,A1(去掉 refuse_unapproved 的 refused)只红审批门的运行时一半。

门禁 check-runs(钉 head):Build and Test (22.x) ✓ · Quality Checks ✓ · Check Changeset ✓ · Playwright ✓ · link-check ✓ · CodeQL ✓ · Analyze Code (javascript) ✓ · Label Pull Request ✓ · Vercel Preview Comments ✓ — 9/9 success,mergeable_state clean,base abc16634 = 当前 origin/main。Typecheck and Build(docs-app.yml)按路径过滤,本 PR 不触及 content/docs/**,缺席属正常。

接受的偏差:① 「文案不变」按对话框里原有的字读:end 节点没有标题,拒绝对话框标题是动作名,所以 message 以原标题开头,否则「Conversion refused」会从对话框消失、docs/MAINTENANCE.md 那句随之失真。② 第 2b 项 refuse_nonpositive_amount 的注释改了一句(原文说「三个拒绝仍是消息屏」,本 PR 使之失真),其配置与边逐字未动。③ 行数超预算,原因见上。④ refuse_unapproved 在控制台只能从过期页面到达(convert_lead.visible 会隐藏按钮),dev 用 --fresh 临时库上的系统写构造夹具测量,属设计如此。

范围外去向:拒绝对话框标题用动作名、暂停屏用流程名——控制台的取舍,与 refuse_held 一致,只记在 PR 的验收说明里。

落地条款:席位经中继 pr_ready + automerge_enable,以 objectstack-fleet[bot] 入合并队列;合并后 #2060 退回 pm:queue(Part of),按串行口径派下一项(第 3 项)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 01:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit f1c8083 Oct 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants