Skip to content

contracts.mdx「手动终止」小节两句都不成立:所有者未必能改状态,已终止的合同也拦不住被复活 #872

Closed

Description

@yinlianghui

来源:#832 实施过程中的顺带发现。#832 的裁定面是该页四处(列表视图、幽灵选项、内置规则、给销售代表的提示),本条是同页第五处,PM 的认领评论把边界划在那四处,故单独立单。

文件面:content/docs/revenue/contracts.mdx:120-121 +(PR #874 落地后行号会下移)zh-Hans / zh-Hant 同处。

页面写的

### Manual termination
The owner can change status to Terminated. This is a one-way action —
terminated contracts can't be reactivated. Audit log records who and when.

两句都不成立

1. 「所有者可以把状态改成 Terminated」——要看这位所有者是谁。 crm_contract 的编辑权是对象级的,与是不是所有者无关:src/profiles/sales-rep.profile.ts:52 是 allowEdit: false,销售代表改不了自己名下的合同;能改的只有 sales-manager.profile.ts:24 与 system-admin.profile.ts:28。而合同所有者恰恰经常是销售代表——src/objects/quote.hook.ts:141-144 起草合同时把报价的 owner_id 抄了过去。所以这句对最常见的那类所有者是假的。(与 #549 的记录级共享缺口是两回事:这里是对象级 CRUD 门,共享放开也过不去。)

2. 「已终止的合同不能被重新激活」是一道不存在的闸。 contract_status_progression 确实把 terminated 写成终态(src/objects/contract.object.ts:272-273),但整条规则是 severity: 'warning'(:260)。实测引擎侧:@objectstack/objectql 的 evaluateValidationRules 只在 severity === 'error' 时把违规推进 errors 并抛 ValidationError,其余级别只 logger.warn 然后放行(node_modules/@objectstack/objectql/dist/core.mjs:2434-2441)。所以把一份 terminated 的合同改回 activated,服务端记一条日志,保存照样成功;表单上没有任何东西拦它,也不会通知任何人。

影响

与 #832 第 3 条同型(「已激活的合同不能被删除」也是无实现的假护栏):管理员照着它以为终止是不可逆的。PR #874(#832 的实现)已经把同页「内置规则」小节改写成「状态推进是建议,不是闸门」,本节这句与它直接冲突——同一页会给出两个互相矛盾的答案,所以这条值得尽快排。

修法建议

三语按实况改写:谁能改状态(Sales Manager / System Administrator,销售代表不能,哪怕合同挂在他名下),以及「终态」的真实力度(声明为警告级,只记日志不拦保存;要真的不可逆得由管理员另行配置)。⛔ 不要改 src/** —— 要不要把状态机提到 error、要不要放开销售代表的编辑权,都是产品决定,另开实现单。

Refs #832 #874 #549

Activity

  1. added
    documentationImprovements or additions to documentation
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    documentationImprovements or additions to documentation
    on Aug 6, 2026
  2. self-assigned this
    on Aug 6, 2026
  3. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    [PM 认领 · R22] session_01VHrPAGEgFDoHjphqYG4BMa · 分支 claude/issue-872-contracts-termination-sharing · 文件面:content/docs/revenue/contracts.mdx ×3(「手动终止」小节 + 「共享」小节各一两句)+ changeset

    先追加一处同页同类(来自 #832/PR #874 验收裁定):「共享」小节的「合同所有者读取并编辑自己的合同」——sales-rep.profile.ts 对 crm_contract 是 allowEdit:false,而合同所有者常常正是销售代表(quote_on_accepted 把报价 owner_id 抄给合同)。PR #874 改写的「给销售代表的提示」已写明这一点,该句现与之同页矛盾,一并收口。本单共三句。

    裁定与边界:

    1. **「所有者能改状态」**按 profile 实况写实:能改状态的是 Sales Manager / System Administrator;所有者(常为 rep)只读。
    2. 「已终止不能复活」是假护栏:状态机 severity:'warning' 只记日志不拦保存(docs(contracts): 按视图、选项集、hook 与 profile 写实合同页其余四处 (#832) #874 已实测引擎行为并写进同页「内置规则」节)——本句与该节保持一致口径,不再造闸门;「一次性动作」的建议语义可保留,但必须写明没有任何东西强制它。
    3. 「共享」句写实为:记录级共享决定读的范围;编辑权由 profile 的对象级 CRUD 决定,记录共享放开也过不了对象门(Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read #549 的最终成文口径若与此冲突,以 Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read #549 为准——本句只陈述当前实测行为,不预判 Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read #549)。
    4. ⚠️ 基线必须含 PR docs(contracts): 按视图、选项集、hook 与 profile 写实合同页其余四处 (#832) #874(刚合并,同页大改,行号全部重定位);⛔ 不回退 docs(contracts): 按视图、选项集、hook 与 profile 写实合同页其余四处 (#832) #874/docs(contracts): 按到期/续约 flow 与 contract.object.ts 重写三语剩余三处失真 (#826) #831 的任何措辞,新句与「内置规则」节和「给销售代表的提示」互相呼应不重复;⛔ 不动 src/**;不加守卫;三语同步;zh 术语按语言包与该页既有写法。

    ⛔ 不升级 @objectstack/*;不动 releases/;changeset 站内路径反引号;控制字节自扫;JSON 报告按标准 schema。


    Generated by Claude Code

  4. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    [PM 验收] PR #878 ACCEPT — 已转 ready 并挂 auto-merge(session_01VHrPAGEgFDoHjphqYG4BMa)

    复核结论:

    越界发现处置(编号更正见 PR #878 评论区):#880(Sales Manager 对合同有效写深度 own,A/B/C 三路属权限面产品决定)→ needs-user-decision 收件箱;#881(zh 共享收尾段停在 #699 前,建议连带核同批两页)→ pm:queue。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions