Skip to content

feat(revenue): quotes and contracts follow the account (controlled_by_parent) — #549 - #1956

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-549-owd-controlled-by-parent
Sep 30, 2026
Merged

os-warren merged 2 commits into
mainfrom
claude/issue-549-owd-controlled-by-parent

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #549

Executes the 2026-08-31 director ruling (hotcrm 决裁批 #16 ①) in full: crm_quote and crm_contract both move from sharingModel: 'private' to controlled_by_parent under crm_account; crm_task untouched; 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), a sales_rep holding acct_US ONLY through north_america_territory (sys_record_share acct_US:edit:rule), hasPlatformAdminGrant=false, permissions [member_default, sales_rep].

reading shipped (both private) converted (ruling as written)
read quote_JP by id 0 0 — level one enforces
read line_JP by id 0 0 — level two enforces (pass condition)
write line_JP DENIED PermissionDeniedError DENIED PermissionDeniedError (pass condition)
crm_quote reach [quote_own] [quote_US, quote_own]
crm_quote_line_item reach [line_own] [line_US, line_own]
crm_contract reach [] [contract_US]
write quote_US / line_US DENIED ALLOWED — the accepted cost (territory share carries edit; sales_rep holds allowEdit on quotes)
write contract_US DENIED DENIED — object gate (allowEdit: false)
control oli_own read / write 1 / ALLOWED 1 / ALLOWED (live, both runs)
control oli_JP read / write 0 / DENIED 0 / DENIED

The same readings are now pinned on disk by test/parent-derived-reach.test.ts (positive two-level assertions plus the by-id negatives, with crm_opportunity_line_item under 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_account promoted from a required Field.lookup to Field.masterDetail (deleteBehavior: 'cascade'). Master-detail is REQUIRED for the contract, not optional: it declares a second required lookup (crm_contact), and 17.3.0's security-controlled-by-parent-ambiguous-relation refuses a master decided by declaration order (objectstack#14747, the finding the previous attempt filed). pnpm validate exit 0.
  • src/sales/profiles/sales-rep.profile.ts — readScope: 'own' removed on both objects (inert under controlled_by_parent; the authorization-coverage inert-scope guard demands it, per A3).
  • src/sales/profiles/sales-manager.profile.ts — writeScope: 'own_and_reports' on crm_contract removed 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, which sales_manager holds 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.ts comment updated to say so).
  • src/sales/objects/account.hook.ts — account_protection extended 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 in test/cascade-guard-messages.test.ts and stated in the docs rather than the "drafts cascade" wording the changeset first had.
  • Tests: 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/contract derived), test/hooks-runtime-sales.test.ts (5 new account_protection cases), test/cascade-guard-messages.test.ts (2 new end-to-end cases).
  • Docs, three locales each: 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 the own_and_reports scope 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.ts needed NO edit — its guard applied automatically. Nothing else was touched; crm_task, sharing rules, src/flows/**, package.json untouched.

Verification (every command through the shared lock; exit codes captured by redirect)

Chain from package.json verify, on commit 6d8618e2:

  • pnpm validate exit 0 · pnpm typecheck exit 0 · pnpm lint exit 0 · pnpm lint:i18n-gate exit 0 · pnpm hygiene exit 0 · pnpm hygiene:tokens exit 0
  • pnpm build exit 0 · pnpm test exit 0 — 172 test files passed, 3696 tests passed, 1 skipped (run on 6d8618e, lock verdict command-exit 0, held 142s)
  • Token ratchet before → after (ceilings untouched): src/sales business 54,179 → 54,326 · src/sales authored 99,340 → 99,463 · src/revenue business 15,169 → 15,195 · src/revenue authored 18,279 → 18,305 · marketing/service unchanged.
  • Baseline before any edit: test/parent-derived-reach.test.ts on unmodified 4db37cd4 → 7 passed.

Acceptance notes

维护者速读(草稿)

改了什么 — 报价(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

…_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
@vercel

vercel Bot commented Sep 25, 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 Sep 25, 2026 12:05pm UTC

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces configuration Build and app configuration files backend Server-side behaviour — hooks, flows, actions labels Sep 25, 2026
This was referenced Sep 25, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — #549 record-level security (maintainer-approved).

Maintainer, verbatim, in the repo:hotcrm seat session: 「1956 同意」 — the approval this security/permission-boundary change was held for.

State at acceptance: head 6d8618e2, every check green (Build and Test, Typecheck and Build, Quality Checks, Playwright, CodeQL, link-check, Check Changeset), git merge-tree against current main clean. Marking ready and enabling auto-merge; #549 closes on merge.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 30, 2026 07:21
@os-warren
os-warren added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit c418c5f Sep 30, 2026
11 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 configuration Build and app configuration files documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read

2 participants