Repository navigation
feat(revenue): quotes and contracts follow the account (controlled_by_parent) — #549 - #1956
Merged
Merged
Conversation
…_parent) Convert crm_quote and crm_contract from private to controlled_by_parent under crm_account, promoting the account lookup to master-detail so the master is authored rather than positional. Drop the now-inert readScope (sales_rep) and writeScope (sales_manager) on both objects; extend account_protection to refuse deleting a customer account with an activated contract; re-pin the parent-derived reach test to the two-level chain (line item -> quote -> account) measured on @objectstack 17.4.0, and bring the sharing, contracts and profiles docs (three locales) to the new reach. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8U3asekbiC7yWoEPWR4Dg
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X8U3asekbiC7yWoEPWR4Dg
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Sep 25, 2026
Closed
Contributor
Author
|
ACCEPT — #549 record-level security (maintainer-approved). Maintainer, verbatim, in the State at acceptance: head Generated by Claude Code |
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 #549
Executes the 2026-08-31 director ruling (hotcrm 决裁批 #16 ①) in full:
crm_quoteandcrm_contractboth move fromsharingModel: 'private'tocontrolled_by_parentundercrm_account;crm_taskuntouched; no app-side workaround. The ruling's hard gate — the objectstack#11082 reproduction fixture on the pinned@objectstack/* 17.4.0— was run BEFORE any source edit and is GREEN (table below), so the conversion lands.Security boundary: this PR changes record-level visibility on two objects. It is a draft held for the maintainer's approval and must not be flipped ready by a seat.
Gate verdict (objectstack#11082 fixture, 17.4.0, run on an unmodified tree via an in-test stack override)
Harness: real booted kernel (ObjectQL + plugin-security + plugin-sharing over this app's
objectstack.config.ts), asales_repholdingacct_USONLY throughnorth_america_territory(sys_record_share acct_US:edit:rule),hasPlatformAdminGrant=false, permissions[member_default, sales_rep].quote_JPby idline_JPby idline_JPcrm_quotereach[quote_own][quote_US, quote_own]crm_quote_line_itemreach[line_own][line_US, line_own]crm_contractreach[][contract_US]quote_US/line_UScontract_USallowEdit: false)oli_ownread / writeoli_JPread / writeThe same readings are now pinned on disk by
test/parent-derived-reach.test.ts(positive two-level assertions plus the by-id negatives, withcrm_opportunity_line_itemunder a still-private opportunity as the live control).PM assumption A1 (17.4.0 carries PR #11183) is confirmed by behaviour, not text. A2 confirmed (
src/revenue/objects/). A3 confirmed and followed.What changed
src/revenue/objects/quote.object.ts,contract.object.ts—sharingModel: 'controlled_by_parent';crm_accountpromoted from a requiredField.lookuptoField.masterDetail(deleteBehavior: 'cascade'). Master-detail is REQUIRED for the contract, not optional: it declares a second required lookup (crm_contact), and 17.3.0'ssecurity-controlled-by-parent-ambiguous-relationrefuses a master decided by declaration order (objectstack#14747, the finding the previous attempt filed).pnpm validateexit 0.src/sales/profiles/sales-rep.profile.ts—readScope: 'own'removed on both objects (inert under controlled_by_parent; theauthorization-coverageinert-scope guard demands it, per A3).src/sales/profiles/sales-manager.profile.ts—writeScope: 'own_and_reports'oncrm_contractremoved for the same reason. Consequence for Sales Manager 对 crm_contract 有 allowEdit 却没有写深度:报价自动起草的合同(挂在 rep 名下)只有 System Administrator 改得了 #880: the manager's write on a contract now derives from EDIT on the account, whichsales_managerholds org-wide (modifyAllRecords), so a manager edits every contract — including the rep-owned contract an accepted quote drafts — on every edition, without the enterprise hierarchy resolver. The app now authors NO hierarchy scope.requires: ['hierarchy-security']stays (the HotCRM lists hierarchy-security in requires[] but declares ZERO hierarchy readScope — the read path of a required capability is never exercised (cloud#1628 census) #1378 refusal to remove it stands;objectstack.config.tscomment updated to say so).src/sales/objects/account.hook.ts—account_protectionextended per the 2026-08-02 ruling: refuses deleting a customer account while an Activated contract references it (DELETE_RESTRICTED/ 409, singular and plural sentences). Measured end-to-end: an account with only a draft contract is ALSO refused, by the platform's referential pass (the contact cascade stops on the contract's required Primary Contact) — pinned intest/cascade-guard-messages.test.tsand stated in the docs rather than the "drafts cascade" wording the changeset first had.test/parent-derived-reach.test.ts(re-pinned to the new truth, not deleted),test/contract-write-depth.test.ts(re-taken for Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read #549: manager edits a rep-owned contract on the open edition; rep still refused by the object gate; DELETE still refused),test/hierarchy-read-depth.test.ts(authored hierarchy writes now[]),test/sharing-coverage.test.ts(ledger: quote/contractderived),test/hooks-runtime-sales.test.ts(5 new account_protection cases),test/cascade-guard-messages.test.ts(2 new end-to-end cases).content/docs/administration/sharing-and-security*.mdx(OWD rows, count sentence six → eight, related-list rows, prose, FAQ),content/docs/revenue/contracts*.mdx(Sharing section and the "Who can edit which contract" table re-taken; manual-termination and delete paragraphs; rep tips),content/docs/administration/profiles*.mdx(rep bullets)..changeset/quotes-and-contracts-follow-the-account.md— minor, FROM → TO per persona.File-surface declaration (breach, explained)
The dispatch expected
src/revenue/objects/{quote,contract}.object.ts, the account-protection hook,test/authorization-coverage.test.ts,test/parent-derived-reach.test.ts, one changeset. Landing green required more, all of it the mechanical follow-on the 2026-09-02 ACCEPT review (comment 5518049395) already listed for "the next dispatch": the two profiles (the inert-scope guard goes red otherwise — A3),test/sharing-coverage.test.ts(ledger) and its three-locale OWD / related-list docs guards,test/contract-write-depth.test.ts+test/hierarchy-read-depth.test.ts(both pin theown_and_reportsscope this conversion makes inert), the contracts and profiles doc pages (they describe the old reach in prose the guards and readers hold to),objectstack.config.ts(comment only).test/authorization-coverage.test.tsneeded NO edit — its guard applied automatically. Nothing else was touched;crm_task, sharing rules,src/flows/**,package.jsonuntouched.Verification (every command through the shared lock; exit codes captured by redirect)
Chain from
package.jsonverify, on commit6d8618e2:pnpm validateexit 0 ·pnpm typecheckexit 0 ·pnpm lintexit 0 ·pnpm lint:i18n-gateexit 0 ·pnpm hygieneexit 0 ·pnpm hygiene:tokensexit 0pnpm buildexit 0 ·pnpm testexit 0 — 172 test files passed, 3696 tests passed, 1 skipped (run on 6d8618e, lock verdict command-exit 0, held 142s)test/parent-derived-reach.test.tson unmodified4db37cd4→ 7 passed.Acceptance notes
src/sales/profiles/sales-manager.profile.tsstill carries pre-rc.4 comments oncrm_campaign_memberand the line-item grants ("quote lines are org-wide", "the master set comes from the master's RLS policies only") thattest/parent-derived-reach.test.tshas contradicted since 17.0.0-rc.4. Stale prose, no reader; carrier: whoever next edits that file.pnpm validateprints the expected informational line thathierarchy-securityis provided by@objectstack/security-enterprise; the app now authors no hierarchy scope, so the declaration is dormant until one is authored again. Removing it is the HotCRM lists hierarchy-security in requires[] but declares ZERO hierarchy readScope — the read path of a required capability is never exercised (cloud#1628 census) #1378 refusal's call, not this PR's.维护者速读(草稿)
改了什么 — 报价(
crm_quote)与合同(crm_contract)的组织级默认从「私有」改为「由父级控制(客户)」,客户字段升为主从关系;销售代表档案上两处失效的readScope、销售经理档案上失效的合同writeScope删除;客户删除守卫扩展到「仍有已激活合同」;三种语言的共享/合同/档案文档同步;测试按实测重钉。为什么改 — 执行 2026-08-31 总监席裁决(8/2 原裁全量执行):通过区域/团队规则拿到客户的代表,现在能在客户页看到该客户的报价与合同相关列表(含续约),而不是只看到自己名下的。前提门禁(objectstack#11082 夹具,17.4.0)实测绿:读不到的客户下的报价,其行项同样读不到、写不了。
风险与代价(含回滚) — 可见性放宽:能读到客户的人读到其报价与合同;持有客户编辑权的代表可编辑其报价(8/2 裁决已接受的代价,合同仍不可编辑);销售经理在任何版本上都能编辑任何合同(#880 的企业版边界随之消失)。回滚 = revert 本 PR 的 squash 提交,无数据迁移(客户字段本已必填)。
席位意见 —(留空,席位定稿)
你要做的 — 确认接受上述放宽后合并;不接受则关闭本 PR,卡回
pm:on-hold并说明理由。Generated by Claude Code