Repository navigation
fix(intake): route web leads and cases, flag web duplicates (runAs: 'system') - #2103
Merged
Merged
Conversation
…: 'system') lead_auto_assign, case_auto_assign and lead_duplicate_check read the rep / agent pool and existing contacts and leads as the anonymous form caller, whose guest_portal grant reads nothing, so every web lead and case landed ownerless and a web duplicate was never flagged. Since objectstack#22437 (18.0.0-next.0) the form door answers the new id only, so the three hooks now elevate their reads (ruling 6076828219 item 4, B). Each read is pinned to the organization the writer named on the new record (AGENTS.md rule 10). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
… only Flip the pins that recorded F9 as a defect to the business fact: a web case and a web lead are round-robined into their pool, a web duplicate is flagged, and a planted owner gives way to the pool's choice. Through the platform's own anonymous door, both public forms answer the new id and nothing else. Two-organization cases pin each elevated read to the organization the writer named. Comments that said the form cannot read the pool are corrected. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
…anted The shared address was computed after the fixture helper advanced the counter, so the intake never repeated the other organization's contact and the dedupe half of the case could not fail. Measured by ablation: with the organization pin removed it stayed green. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
The setup page said the anonymous web-to-case form cannot read the agent pool, and the leads page said a web duplicate lands unflagged; both stopped being true when the intake hooks elevated their reads. Both now say the visitor is told only that the submission was received. All three locales, plus the release note. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
The two comments in lead_auto_assign said a lead the round-robin could not place is routed downstream by a manager or the lead_assignment flow. No flow routes an ownerless lead (#2060 item 6), so the comments now say only that the lead is still captured, ownerless. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
#2101's new ownerless-lead describe said web-to-lead lands ownerless when the anonymous form grant cannot read the rep pool; since the intake hooks read it elevated, only an empty pool or a failed read leaves it ownerless. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 14, 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 #2014
Clause-②: no
F9, the card's last item: the three web-intake hooks
lead_auto_assign,case_auto_assignandlead_duplicate_checknow declarerunAs: 'system', per maintainer ruling6076828219item 4 (B: the platform fixed the anonymous form door first, objectstack#22437, installed in 18.0.0-next.0; hotcrm then elevates the three hooks). Web-to-Lead and Web-to-Case submissions are routed into their pools, a web duplicate is flagged, and the anonymous answer still carries only the new record's id.Measured first (dispatch hypotheses H1 to H5)
On a dev server of my own on port 4105,
@objectstack/*18.0.0-next.0,pnpm demo:staffrun (2sales_repholders, 1service_agentholder). The submitted email repeats an existing contact's address.e094ec71POST /api/v1/forms/contact-us/submit(anonymous)["id"]["id"]POST /api/v1/forms/support/submit(anonymous)["id"]["id"]owner_idnull,duplicate_statusnullsales_repholder,duplicate_status: suspected,duplicate_of_contactis the known contactowner_idnullservice_agentholder@objectstack/rest18.0.0-next.0 isres.status(201).json({ id: result.id }).GET /api/v1/data/crm_lead/:idandPOST /api/v1/data/crm_lead/queryboth answer401(in-process, real Hono app). Browser probe (Playwright, DOM text): an anonymous visitor submits/_console/f/contact-uswith a known address. The page reads only "Submitted Thank you! Your submission has been received successfully.", with no staff name and no duplicate wording, and the submit response is201with keys["id"].beforeInsert,organization_idis ABSENT frominputfor every writer: an anonymous submission, a system seed, and a rep's insert. The platform derives the organization after the beforeInsert hooks (ADR-0131;resolveSystemInsertOrganizationsays so in@objectstack/objectql), so the stored row carries the Default Organization. With two organizations the anonymous door refuses the insert outright (ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED).sudo()keepstenantId, and the driver scopes on it even forisSystem). Measured: a rep in org A inserting a lead whose address is held by a contact in org B does not get B's contact as the match.lead_auto_assign(250) andcase_auto_assign(250) still run after the guest strips (200). A planted owner gives way to the pool's choice: the pins now plant a pool member who is the MORE loaded one, and assert the less-loaded one.The change
src/sales/objects/lead.hook.ts:lead_auto_assignandlead_duplicate_checkdeclarerunAs: 'system'.src/service/objects/_case-assignment.ts:createCaseRoundRobinAssign(case_auto_assign) gets the same, in round 1's pattern. The F14 escalation hook beside it is untouched.duplicate_of_contactorduplicate_of_lead. This is option C's "reps get cross-rep dedupe" side, which ruling B also elevates.Tests (pins flipped to the business fact, plus the acceptance pins)
test/case-assignment.test.tsverify.api('/forms/support/submit'), which the handle serves since 18.0.0-next.0) is routed to the staffed agent, and the answer's keys are exactly["id"].test/hooks-runtime.test.tsverify.api('/forms/contact-us/submit')is routed AND flagged, and the answer's keys are exactly["id"].test/hooks-runtime-service.test.ts: the guest dedupe and guest assign cases are flipped, the two guest-strip cases assert the pool's owner, and there is a new rep cross-owner dedupe case.test/guest-submission-sanitisation.test.ts: the planted case owner is replaced by the agent, a positive value instead of an absence.test/flow-record-change.test.ts: the web lead lands on the rep and its hot-lead alert reaches them (the "web leads alert nobody" half of F9).Ablation (one-off,
scripts/ablation-replace.mjsfrom objectstack; each leg mutated, ran and was restored, blob == HEAD,git diff HEADempty, final tree clean)case_auto_assignrunAs set toinherit: 5 red (the routing, spoof and door cases in case-assignment; guest-sanitisation; the hooks-runtime-service case strip).lead_auto_assignrunAs set toinherit: 5 red (both hooks-runtime web cases, the hooks-runtime-service strip and door cases, flow-record-change).lead_duplicate_checkrunAs set toinherit: 3 red (the door case, the guest dedupe case, the rep cross-owner case).204e8578, and the re-run leg is red.runAs: 'inherit'and are reported above.Docs (three locales each)
content/docs/administration/setup*.mdx, case routing: the sentence that the anonymous web-to-case form cannot read the pool is replaced. A web case is routed like any other, and the visitor is told only that it was received.content/docs/sales/leads*.mdx, duplicate callout: "the lead lands unflagged" is replaced. A web enquiry is checked like any other lead, and the form never reveals whether an address is known.content/docs/service/cases*.mdx(~L146, ~L181) were checked against the measurement and are now true as written ("lands while nobody holds the Service Agent position"), so they are unchanged.Surface beyond the claim
content/docs/sales/leads*.mdx: F9 made its duplicate callout false.src/service/views/case.view.ts: one comment said the public-form context cannot readsys_user_position.test/helpers/verify-stack.ts(the handle now serves the form route),test/demo-staffing.test.ts,test/ownership-model.test.ts.test/hooks-runtime.test.ts,test/hooks-runtime-service.test.ts,test/guest-submission-sanitisation.test.tsandtest/flow-record-change.test.ts.lead_auto_assign's two comments no longer say a manager or thelead_assignmentflow routes an unplaced lead.origin/mainwas merged twice (6782f782, thenc0f3485b, which is fix(lead): an ownerless lead's routing run ends after its SLA stamp instead of failing #2101). In the merge, fix(lead): an ownerless lead's routing run ends after its SLA stamp instead of failing #2101's new test comment, which said the anonymous form grant cannot read the pool, now says an empty pool or a failed read.Acceptance notes
src/sales/flows/lead-assignment.flow.tslines 15-16 (from fix(lead): an ownerless lead's routing run ends after its SLA stamp instead of failing #2101) still saylead_auto_assignstands down on "a pool the writer cannot read". After this PR the writer's grant no longer decides that, only an empty pool or a failed read does. That file is outside this card (src/sales/flows/**), so it is left as is. Carrier: the next PR that touches the flow.singleposture: the anonymous form is still offered, and every submission answers500withERR_SYSTEM_WRITE_ORGANIZATION_REQUIREDand "Internal server error". This was measured in-process through the real Hono app. The platform declares that topology unsupported (the next boot refuses it), so this is noted only.duplicate_of_contactthe rep cannot read was not measured.Gates
pnpm verifyon head407b2219(after the second merge):OS_VERIFY_LOCK_SLOT=hotcrm-2014 bash os-verify-lock.sh -c 'pnpm verify'→os-verify-lock: VERDICT command-exit 0 · held the lock 539s.✓ Validation passed;tsc --noEmitclean;objectstack lint --strict18 suggestion(s), no error;✓ i18n lint gate: 0 i18n/missing-* issues;✓ source hygiene clean;✓ source token ratchet clean(src/sales business semantics ~58,041 of 88,500);✓ Build complete.Test Files 179 passed (179),Tests 3676 passed | 1 skipped (3677).57950e8b, before fix(lead): an ownerless lead's routing run ends after its SLA stamp instead of failing #2101 landed: 3664 passed, 1 skipped.test:coveragethresholds (CI runstest:coverage, localverifyrunstest), docs-app, e2e, link-check, code-quality.Generated by Claude Code