Skip to content

[17.0-rc2验收] quote_on_accepted 把 boolean false 传入合同 lookup:凡缺 contact 或 opportunity 的报价被接受后,合同不起草、close-won 也不执行(静默失败) #714

Description

@yinlianghui

现象

接受(accept)一张没有同时挂 crm_contact 和 crm_opportunity 的报价时,quote_on_accepted hook 的自动起草合同 insert 抛 ValidationError,整个 handler 中止:

  • 合同不会起草;
  • 即便报价挂了 opportunity(只缺 contact),第 130 行的 close-won 分支也执行不到 —— 成交不推进;
  • hook 是 async: true + onError: 'log',接受写本身照样 200,用户完全无感知。

服务器日志(两次独立复现均出现):

ERROR [hook] handler failed (onError=log; suppressing) {"hook":"quote_on_accepted","object":"crm_quote",
"event":"afterUpdate","error":"hook 'quote_on_accepted' threw: ValidationError: Primary Contact has an
invalid lookup value: Invalid input: expected string, received boolean; Related Opportunity has an
invalid lookup value: Invalid input: expected string, received boolean"}

复现步骤(×2 均复现)

# 只挂 account 的报价,合法状态链推进到 accepted
POST /api/v1/data/crm_quote {"name":"Q","crm_account":"<acct>","status":"draft","expiration_date":"2026-12-31","total_price":1234}
PATCH .../crm_quote/<id> {"status":"in_review"}   # 200
PATCH .../crm_quote/<id> {"status":"presented"}   # 200
PATCH .../crm_quote/<id> {"status":"accepted"}    # 200(写不被阻断)
# 等异步 hook 后:
GET /api/v1/data/crm_contract?crm_account=<acct>  # → 0 行(两次运行均 0)

对照组:同一环境下 quote 同时挂 contact + opportunity(proposal 阶段),同链路接受 → 合同 1 份自动起草、opportunity closed_won、win_reason=quote_accepted,全部正常 ✅ —— 说明失败特异于「链接缺失」。

期望 vs 实际

  • 期望:CPQ 主链「接受报价 → 起草合同(+ 有关联时 close-won)」对 crm_contact 缺失是健壮的 —— crm_quote.crm_contact 本来就是有意 optional 的(schema 注释:"Recipient is nailed down by the time a quote is presented, not when it is drafted");
  • 实际:任一链接缺失即整链静默失败。

落点分析(hotcrm 侧,hook 代码)

src/objects/quote.hook.ts:98-103:

const contactId =
  (typeof input.crm_contact === 'string' && input.crm_contact) ||
  (typeof previous?.crm_contact === 'string' && previous.crm_contact);

两个操作数都不成立时表达式求值为 false(boolean) 而非 undefined;113-116 行把它原样传进 api.object('crm_contract').insert({ crm_contact: contactId, crm_opportunity: opportunityId, ... }),引擎 lookup 校验拒绝 boolean → insert 抛错 → handler 中止,close-won 分支(130 行起)永远到不了。同文件 94-97 行 accountId 同 idiom,只是被 required 掩盖。

另注(修复时需一并决策):即便把 false 改成 undefined,contact-less 报价起草合同仍会撞上 crm_contract.crm_contact 的 required + notNull —— quote 有意允许无 contact,而合同不允许,二者的设计冲突是这条链对该类报价从未成功过的另一半原因。

非 #684(那是 record-change flows 缺 runAs:'system' 的系统驱动写被拒);本条是 hook 自身的 JS 表达式缺陷,报错形态(ValidationError: received boolean)与 #684 完全不同,两仓查重 quote_on_accepted / "received boolean" / "Auto-drafted" 均无既有单。

环境

hotcrm@0899b4f + @objectstack 17.0.0-rc.2(REST 验收,admin token,独立 file:SQLite 实例)

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 5, 2026
  2. self-assigned this
    on Aug 6, 2026
  3. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    CollaboratorAuthor

    [PM 认领 · 修复线 R39] session_01VHrPAGEgFDoHjphqYG4BMa · branch claude/issue-714-quote-accepted-false-lookup

    文件面:quote 接受路径的 flow/hook(quote_on_accepted)+ 回归测试。

    裁定:

    1. 先在 fresh origin/main(rc.3)premise-check 重验;
    2. 症结按 issue 证据是 && 取值把 boolean false 当 lookup 值传入——最小修复方向为缺失时不传/传 null,恢复合同起草与 close-won 两条路径;若「缺 contact/opportunity 时合同该不该起草」是产品语义分叉,不猜,报 needs-user-decision;
    3. 加回归测试覆盖缺 lookup 的接受路径(静默失败不许再犯);
    4. ⛔ 不升级 @objectstack/*、⛔ 不写平台 workaround——若引擎对 false/null 的处理本身违背 spec,停手报证据,PM 走上游镜像。

    Generated by Claude Code

  4. yinlianghui commented on Aug 7, 2026

    @yinlianghui
    CollaboratorAuthor

    [PM 验收 · R39] ✅ 通过。PR #1013 已转 ready 并武装 auto-merge。

    • premise = partial,且比"部分成立"更有价值:代码缺陷原样在(quote.hook.ts:119-127 的 && 链落成 boolean false),但 rc.3 的 ADR-0104 warn-first 让症状变形——默认 posture 下 false 被放行落库写脏 reference 列(静默污染,比 rc.2 的报错更隐蔽);strict 下才复现 issue 原文报错,dev 两种 posture 都实测钉死。close-won 被吞单独取证(修前 update calls: 0,修后 closed_won + calls: 1)。
    • 修法干净:pickId() 把"没有 id"的所有形态收敛为缺键;起草合同与 close-won 解耦为两条独立的腿,失败各自收集后统一抛出,onError:'log' 从此记得到真实原因。
    • 13 例三层测试(闭包形态/QuickJS 下发边界/真引擎判决),反向验证 11 红 2 绿,连"该绿的 2 例为何绿"都论证了(只量引擎不量 hook,平台行为没变)。
    • 报告点名的语义分叉(crm_quote.crm_contact 可选 vs crm_contract.crm_contact 必填,schema 注释的意图无人执行)不阻塞本单——机械缺陷已修完,失败已变诚实。PM 将另立 needs-user-decision 单载 A/B/C 三选项与 dev 推荐 B 的理由。
    • 越界发现 [finding] 两个 hook/action 测试 harness 都不校验 lookup 取值形态,junk 值(false/number/object)写进引擎照样绿 #1016(harness 不校验 lookup 取值形态,本单正是被该盲区放行)分类正确,留 finding 池。

    Generated by Claude Code

  5. added a commit that references this issue on Aug 10, 2026
    d57124d
  6. added
    priority:p1High: required for production / M2
    and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions