Refuse conversion of a confirmed-duplicate lead - #1555
Merged
os-sales merged 2 commits intoSep 3, 2026
Conversation
`lead_conversion` now stops on a lead whose `duplicate_status` is `confirmed` — a verdict a person recorded — and shows a refusal naming that verdict and where the surviving record is linked. The machine's `suspected` guess keeps #1207's warn-and-allow: `lead_duplicate_check` matches on email equality, so shared inboxes false-positive by construction, and blocking on a guess would need an override flag. The refusal is a screen node rather than the action's `visible` / `disabled` predicate: those are bare booleans with nowhere to put a sentence, and the ruling requires the refusal to name the verdict and the survivor. The flow is also the only choke point the record-header button, the list-row button and the `action_convert_lead` AI tool all pass through. `e22` ("Clean") is narrowed in the same change, and that half is load-bearing: a `decision` node with no declared conditions takes every out-edge whose condition holds, so the old `!= "suspected"` spelling would have shown the refusal and converted the lead in one run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
The three locale pages describe Convert as always available on a qualified lead. It now opens a refusal on a lead whose Duplicate Status is Confirmed, so each page says so under its own conversion heading, using the language pack's own wording for the field, its Confirmed option and the Duplicate Management group. Adds the changeset: this is a behaviour change, and it announces that the app begins refusing conversion of confirmed-duplicate leads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-sales
marked this pull request as ready for review
September 3, 2026 11:42
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1288
Executes ruling C, 2026-08-31 (maintainer 「其他同意」, hotcrm 决裁批 #19 ①): a lead whose
duplicate_statusisconfirmedis refused conversion; a lead flaggedsuspectedkeeps #1207's warn-and-allow.Verified on
4d097f76:pnpm verifygreen end to end —validate·typecheck·lint·lint:i18n-gate·hygiene·hygiene:tokens·build·test(159 files, 3359 passed, 1 skipped).The layer, and why
The ruling allowed either door — 「谓词或 flow 拒绝」 — and also required the refusal copy to name the verdict and the surviving record. Measured against
@objectstack/spec17.2.0, the action predicates cannot do the second half:Action.visibleboolean | CEL envelopeAction.disabledboolean | CEL envelopeAction.errorMessagescreen.descriptionThe flow is also the only choke point that covers every door: the record-header button, the list-row button and the
action_convert_leadAI tool all dispatchPOST /automation/lead_conversion/trigger, whilevisible/disabledare console-side and say nothing to the other two.So the refusal is a message-only
screennode,refuse_confirmed_duplicate, reached by a newConfirmededge (e25) out ofdecision_duplicateand leading straight toend. Nothing in this flow writes beforescreen_1, so the refusal branch cannot create anything.Measured on the shipped console bundle (
@objectstack/console17.2.0,RecordDetailViewflow handler): a run that PAUSES returns{ success: true, silent: true }, andsilentsuppresses the success toast — soconvert_lead'ssuccessMessage: 'Lead converted successfully!'does not fire behind this dialog.The one-line change that is not cosmetic
e22("Clean") was!= "suspected". Adecisionnode that declares noconfig.conditionsreports no branch, so traversal takes every out-edge whose condition holds, in parallel. Left as it was, a confirmed lead satisfiede22ande25— the refusal would have shown and the lead would have converted in the same run. It now excludes both verdicts, and the exclusivity is pinned on the real evaluator across all seven record shapes a driver can produce.What the rep sees
The verdict is named in the vocabulary the record publishes (
duplicate_status's label is "Duplicate Status", itsconfirmedoption's label is "Confirmed", in all four locales). The survivor is named through the relationship fields that exist to carry it — theduplicatesfield group, by its shipped label "Duplicate Management" — which is the house rule oftest/record-id-not-in-prose.test.ts, and the only claim that stays true on theerasedtombstone.suspectedwarning uses. That claim is safe there because onlylead_duplicate_checkwritessuspectedand it matches on email.confirmedis written by a person, and the lead form lets a reviewer pointduplicate_of_type+ its lookup at any record they like — so the email is not guaranteed to be shared.⛔ No override hatch is offered, and the vocabulary of
duplicate_of_typeis deliberately not transcribed into the sentence ("an existing Lead" / "an existing Contact"): those are locale-pack facts with one source of truth, visible on the section this line points at, and a hand-copied machine list in prose is the drift AGENTS.md documentation rule 5 forbids.Three states, pinned — with the red produced before each green
test/lead-duplicate-visibility.test.ts. Each pin resumes the run and then counts what the store holds: a paused run has created nothing on any branch yet, so a pin that stopped at the pause would be green against a flow with no refusal in it.suspectedconverted)e21at the refusal node (option B) → 4 redconfirmedis_converted: false, status stillqualified; all three survivors (lead · contact ·erasedtombstone)e25back atno_duplicate_warning(the before-picture) → 5 rede22at the refusal node → 4 rede22to!= "suspected"→ 2 red,expected [ 'e25', 'e22' ] to have a length of 1 but got 2Every ablation proved its mutation reached disk (anchored counts on removed and injected text, plus a blob hash differing from the HEAD blob), and every restore was proved by state (
git diff HEADempty, blob hash back to its HEAD value) rather than by an exit code. One of the four initially reportedMUTATION DID NOT REACH DISKon a mis-escaped anchor and its reading was discarded rather than trusted.Before-picture, measured on the parent commit
Through the same harness,
confirmedleads suspended onscreen_1with no description at all (the #1207 warning is gated onsuspected) and then converted: one account, one contact, one opportunity,is_converted: true. All three ofcrm_lead/crm_contact/erasedsurvivors did. That is the behaviour the changeset announces changing.Not touched
src/translations/**— flow copy has no locale channel in this repo (noflowskey in any pack;lead_conversionappears 0 times in all four). Action copy is localized, but no action string changed.src/objects/lead.hook.tsandsrc/objects/lead.object.ts— out of surface.Docs
The three
content/docs/sales/leads*.mdxlocales described Convert as always available on a qualified lead. Each now says, under its own conversion heading, that a Confirmed duplicate opens a refusal instead — using the language pack's own wording for the field, its Confirmed option and the Duplicate Management group.🤖 Generated with Claude Code
https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
Generated by Claude Code