Repository navigation
fix(opportunity): a large deal closes won only once its approval is approved (#2350) - #2373
Conversation
…pproved (#2350) Ruling B: at or over LARGE_DEAL_AMOUNT, opportunity_lifecycle refuses a user's move to closed_won unless approval_status is approved, so not_required and an unset verdict refuse as pending and rejected do. This closes recall-then-close and raise-and-close-in-one-save. quote_workflow's acceptance mirror holds the same states, so an acceptance is refused up front rather than its close-won one write later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAopZCZodPbuxbHDRnB
…parity wording (#2350) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAopZCZodPbuxbHDRnB
…flow-off limit (#2350) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAopZCZodPbuxbHDRnB
…deal (#2350) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAopZCZodPbuxbHDRnB
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
席位核对Verdict: ACCEPT ·
Generated by Claude Code |
Requested by Irene · project thread
Fixes #2350
Clause-②: no
Metadata-protocol: none
维护者速读
opportunity_lifecycle的大额赢单拒绝从"审批中或已驳回"扩大为"除已批准以外的一切":金额 ≥ 100,000 的商机,审批状态不是approved(含not_required和空值)时,用户改为 Closed Won 返回RECORD_LOCKED/ 409,界面提示"先不成交地保存商机以送审"。报价接受的镜像拒绝(Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014)同步扩大,文档三语更新并写明"关闭审批流程"的已知限制。src/sales/objects/opportunity.hook.ts(Large Deal Approval 拒绝块与 KNOWN LIMIT 注释)、src/revenue/objects/quote.hook.ts(接受镜像的holds)、test/opportunity-approval-resubmit.test.ts(新 A large deal can still close won with no approval while its Approval Status is Not Required: recall it and then close it, or raise the amount and close in one save #2350 块)。pnpm exec vitest run test/opportunity-approval-resubmit.test.ts,预期 25 条全部通过;或在控制台以销售身份把种子商机 Acme Platform Upgrade(150,000,无需审批)直接改为 Closed Won,应弹出 "A deal of 100,000 or more needs Large Deal Approval before it can be Closed Won…"。What changes
opportunity_lifecycle(src/sales/objects/opportunity.hook.ts), the fix(opportunity): a large deal whose approval was rejected can never be resubmitted, and closes won with no approval #2334 clause. A user write (ctx.user?.id) that moves a deal intoclosed_wonwith an amount at or overLARGE_DEAL_AMOUNT(both read input-first) is now refused unlessapproval_statusisapproved. Before, onlypendingandrejectedrefused. Nownot_requiredand an unset verdict refuse too, which closes both routes the card names:The answer is still
RECORD_LOCKED/ 409. It now follows the Browser pass #2320 on 18.2.0 — HotCRM-side defects (13 items: dashboards shown to personas with no access, a dead-end case close, campaign editors who cannot edit, a toolbar action that always fails, …) — list card, split per item at dispatch #2338 two-reader shape. Themessageis the diagnostic with API names (… only once approval_status is approved; amount is 150000, approval_status is not_required.). TheuserMessageis the console sentence in labels: "A deal of 100,000 or more needs Large Deal Approval before it can be Closed Won, and its Approval Status is Not Required. Save the deal without closing it to send it for approval; it can be Closed Won once approved." Pending and Rejected keep their own next step. The printed line comes from the body's oneLARGE_DEAL_AMOUNTcopy.quote_workflow(src/revenue/objects/quote.hook.ts), the Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014 acceptance mirror. This was added to the Claim's file surface by the seat. B makes the old mirror false: a quote on a largenot_requireddeal would be accepted, andquote_on_accepted's close-won would then hit the new refusal one write later, which is the Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014 split. The mirror now holdsapproval_statuswhen it ispendingat any amount, or anything butapprovedat or over the line. The other two gates are unchanged. For a large deal no request holds, the sentence says "Save the opportunity to send it for approval, then accept the quote once it is approved."Flow-off measurement (the order's "measure first"). On 18.2.0 a hook cannot see whether
opportunity_approvalis active, so the flow-off case is a known limit. It is named in the hook comment, the in-app page, the docs-site approvals pages and the admin automation page. No setting stands in for the reading. The probe ran on the booted app (hotcrmStack) as asales_rep:automation.toggleFlow('opportunity_approval', false)(the service behindPOST /automation/:name/toggle) wrotesys_metadata_activation {metadata_type: 'flow', name: 'opportunity_approval', package_id: 'app.objectstack.hotcrm', active: false}, andisFlowEnabledwent from true to false.isFlowEnabledis the definition'sstatusAND the ledger (@objectstack/service-automation18.2.0).opportunity_lifecyclebody'sctx.apireads under the caller. Run throughrunShippedHookas the rep,ctx.api.object('sys_metadata_activation').find(…)answeredPERMISSION_DENIED, both before and after the toggle.GET /data/sys_metadata_activationas the rep answered 403. The rep'sPOST /automation/opportunity_approval/toggleanswered 403 ("requires themanage_metadatacapability").Docs:
src/docs/crm_sales_opportunities.mdand.zh.md: the rule, the way forward, the quote consequence and the known limit.content/docs/revenue/approvals.mdx,.zh-Hans,.zh-Hant: a new "Closing a large deal won" section (anchorclosing-a-large-deal-wonin all three), with the Rejected, Recalled and tips bullets pointing at it.content/docs/sales/quotes.*: the acceptance rule namesNot Requiredafter a recall or import, and the new sentence.content/docs/administration/automation.*: one sentence that switching the flow off leaves no way to close a large deal won by hand.Tests:
test/refusal-envelope.test.ts: the shipped-path and in-process cases for the newuserMessage. The site count stays 35: it is still one call site.test/refusal-labels.test.ts:VERDICT_LABELSand the quote mirror'sVALUE_LABELSare held tocrm_opportunity.approval_status's option labels, and the stage and field labels are held too.test/deal-threshold-parity.test.ts:HOOK_COPIESwording.Changeset (patch).
Tests
test/opportunity-approval-resubmit.test.ts, new block "closing won at or over the line needs an APPROVED verdict (A large deal can still close won with no approval while its Approval Status is Not Required: recall it and then close it, or raise the amount and close in one save #2350)". It runs on the booted app with real approvals-door decisions:RECORD_LOCKED/ 409). The deal stays atproposalandnot_required.next_steponly) then opens a pending request, which is the way forward the sentence names.not_required,pending,rejected,approvedand null.test/hooks-runtime-sales.test.ts, Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014 block:not_requireddeal refuses the rep's own close-won (the control) and refuses the quote acceptance, naming "Approval Status is Not Required" and "Save the opportunity to send it for approval". The quote stays presented and no contract is drafted.scripts/ablation-replace.mjs, with agrep -cproof on disk (injected 1, anchor left 0) and atraprestore. The file was restored withgit checkout HEAD --, and the blob hash was checked equal to HEAD's after every run:false && …): cases 1, 2 and 3 red; 6 green.pending || rejected): cases 1, 2 and 3 red. This reproduces the card.not_requiredacceptance case red ("a quote on an unapproved large deal was accepted: expected null to match …").Acceptance notes
not_required:Acme Platform Upgrade(150,000, proposal) andWayne Enterprise License(1,200,000, negotiation). They are the deliberate approval exhibits insrc/sales/data/sales.seed.ts. No test, e2e spec ordemo:staffstep closes either one as a user:test/demo-staffing.test.tsreads them, and the e2e close-won deals are 10,000 to 40,000. So no seed row changes. On a demo box their owner's first save sends them for approval, as before. A direct drag to Closed Won is now refused with the sentence above.POST /data/crm_opportunitywithstage: closed_won,amount: 250000answers 201. It storesnot_requiredand opens no request, because the clause judgesbeforeUpdateonly (as fix(opportunity): resubmit a rejected large deal on the next save, refuse its close-won, and stop recall re-submitting #2342 did) and the insert flow excludes closed stages. Refusing it would also refuse a user's import of historical won deals of 100,000 or more. Whether B covers creation is the maintainer's call.approved, that write meets this refusal, as it already did forpendingandrejected. Shipped inert, unchanged here.userMessagechannel in the console was measured for this hook's other refusal under Browser pass #2320 on 18.2.0 — HotCRM-side defects (13 items: dashboards shown to personas with no access, a dead-end case close, campaign editors who cannot edit, a toolbar action that always fails, …) — list card, split per item at dispatch #2338 item 12, and this PR adds no new channel.Verification
Full suite and the verify chain at f0009ca
The first typecheck, at 278f93c, was red on the quote mirror's closure:
dealwas possibly null inholds. It was fixed in f0009ca. Hygiene and the hygiene:tokens run were at 278f93c; the diff since then touchesquote.hook.tsonly. CI runs coverage and e2e.Flow-off probe (scratch, not committed), 18.2.0
🤖 Generated with Claude Code
https://claude.ai/code/session_01J8VVAopZCZodPbuxbHDRnB