From ed4dccee35f273a3ade243a0d47f8abe6a3b720e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 12:55:23 +0000 Subject: [PATCH 1/3] docs(administration): write the state-machine status vocabulary and the last two behavioural claims to source (#921, #940) The state-machines page used lead statuses and opportunity stages that do not exist. `Working` and `Disqualified` are entries in the lead import alias table (src/mappings/lead_import.mapping.ts), not statuses; the real vocabulary is new/contacted/qualified/unqualified/converted, and Contacted and Unqualified had never appeared on the page. `Closed Without Decision` exists nowhere in src, and the real Needs Analysis stage was missing. Both diagrams, both transition tables and the lede are now copied from the state_machine rules. Two route claims were backwards: New -> Converted is in the lead table deliberately (Convert Lead is offered on any open lead and lead_conversion stamps status:'converted'), and Unqualified is not terminal (it re-opens to New). The opportunity section advertised free backwards movement and a reopen path the table declares nowhere. #940: close_date and amount are unconditionally required, not stage-gated; approval starts at $100K with $500K as the Sales Director tier. The won/lost reason claim was already correct and is untouched. The AI Copilot section is replaced with what src can show - no skill or action references a transition table - leaving the platform-side behaviour explicitly unclaimed. administration/setup carried the same phantom stage, and removing it exposed that the missing Needs Analysis had shifted every probability down one row. All three locales. Documentation only; src unchanged. --- ...te-machines-status-vocabulary-to-source.md | 44 +++++++ content/docs/administration/setup.mdx | 6 +- content/docs/administration/setup.zh-Hans.mdx | 6 +- content/docs/administration/setup.zh-Hant.mdx | 6 +- .../docs/administration/state-machines.mdx | 108 ++++++++++-------- .../administration/state-machines.zh-Hans.mdx | 108 ++++++++++-------- .../administration/state-machines.zh-Hant.mdx | 108 ++++++++++-------- 7 files changed, 230 insertions(+), 156 deletions(-) create mode 100644 .changeset/state-machines-status-vocabulary-to-source.md diff --git a/.changeset/state-machines-status-vocabulary-to-source.md b/.changeset/state-machines-status-vocabulary-to-source.md new file mode 100644 index 000000000..071370a1a --- /dev/null +++ b/.changeset/state-machines-status-vocabulary-to-source.md @@ -0,0 +1,44 @@ +--- +"hotcrm": patch +--- + +State-machine docs: write the status vocabulary, the two remaining behavioural claims and the Copilot section to source. + +`content/docs/administration/state-machines` used a set of lead statuses and opportunity +stages that do not exist. *Working* and *Disqualified* are not statuses at all — they are +entries in `src/mappings/lead_import.mapping.ts`, the alias table that maps a legacy +export's wording onto `contacted` and `unqualified` on import, and the product never shows +either word. The real vocabulary is *New / Contacted / Qualified / Unqualified / +Converted*, and *Contacted* and *Unqualified* had never once appeared on the page that is +supposed to be authoritative about them. On the opportunity side, *Closed Without Decision* +exists nowhere in this app and the real *Needs Analysis* stage was missing, so the page +listed seven stages of which one was invented and one was absent. Both diagrams, both +transition tables and the lede now come from the `state_machine` rules themselves. + +Two claims about the shape of the route were backwards. The page gave *New → Converted* as +its example of a **disallowed** move; that edge is in the table **deliberately**, because +Convert Lead is offered on any open lead and the `lead_conversion` flow stamps +`status: 'converted'` at the end — forbidding it would make every conversion from an +unworked lead log a spurious warning. And *Unqualified* was drawn as a terminal state when +it is the one status with a way back (a re-open to *New*), while the opportunity section +advertised free backwards movement and a reopen path that the table declares nowhere. + +Also corrected on the same page: **Close Date** and **Amount** are `required: true` +unconditionally rather than from *Proposal* and *Negotiation* onwards; approval starts at +**$100K** (`src/flows/opportunity-approval.flow.ts`), with **$500K** being the second, +Sales-Director tier rather than the entry threshold — so a $200K deal does go through +approval, which the old wording denied. The won/lost-reason requirement was already +correct and is unchanged. The **AI Copilot** section, which described the Copilot +withholding suggestions until a status allowed the move, is replaced by what can actually +be shown: no skill under `src/skills/` and no action under `src/actions/` references a +transition table, and whether the platform's own agent reads one is left explicitly +unclaimed rather than asserted in either direction. + +`content/docs/administration/setup`'s stage/probability table carried the same phantom +stage, and dropping it exposed that the missing *Needs Analysis* had shifted every +probability below it by one row — *Proposal* and *Negotiation* were showing 40% and 60% +against a source that says 60% and 80%. The table now matches `STAGE_PROBABILITY` in +`src/objects/opportunity.hook.ts`. + +All changes are English, Simplified Chinese and Traditional Chinese. Documentation only — +no metadata changed. diff --git a/content/docs/administration/setup.mdx b/content/docs/administration/setup.mdx index 42f6d13dd..e232b1c34 100644 --- a/content/docs/administration/setup.mdx +++ b/content/docs/administration/setup.mdx @@ -87,11 +87,11 @@ Confirm the 7-stage default pipeline matches your sales process. Stages and prob | --- | --- | | Prospecting | 10% | | Qualification | 25% | -| Proposal | 40% | -| Negotiation | 60% | +| Needs Analysis | 40% | +| Proposal | 60% | +| Negotiation | 80% | | Closed Won | 100% | | Closed Lost | 0% | -| Closed Without Decision | 0% | If your process differs, adjust in **Setup → Opportunity → Stages**. diff --git a/content/docs/administration/setup.zh-Hans.mdx b/content/docs/administration/setup.zh-Hans.mdx index 5e4d5378b..92de4cd40 100644 --- a/content/docs/administration/setup.zh-Hans.mdx +++ b/content/docs/administration/setup.zh-Hans.mdx @@ -87,11 +87,11 @@ description: 让 HotCRM 为你的团队做好生产准备的第一周任务。 | --- | --- | | Prospecting | 10% | | Qualification | 25% | -| Proposal | 40% | -| Negotiation | 60% | +| Needs Analysis | 40% | +| Proposal | 60% | +| Negotiation | 80% | | Closed Won | 100% | | Closed Lost | 0% | -| Closed Without Decision | 0% | 如果你的流程不同,请在 **设置 → 商机 → 阶段** 中调整。 diff --git a/content/docs/administration/setup.zh-Hant.mdx b/content/docs/administration/setup.zh-Hant.mdx index 6b05c1c66..7e61d0d12 100644 --- a/content/docs/administration/setup.zh-Hant.mdx +++ b/content/docs/administration/setup.zh-Hant.mdx @@ -87,11 +87,11 @@ description: 讓 HotCRM 為你的團隊做好生產準備的第一週任務。 | --- | --- | | Prospecting | 10% | | Qualification | 25% | -| Proposal | 40% | -| Negotiation | 60% | +| Needs Analysis | 40% | +| Proposal | 60% | +| Negotiation | 80% | | Closed Won | 100% | | Closed Lost | 0% | -| Closed Without Decision | 0% | 如果你的流程不同,請在 **設定 → 商機 → 階段** 中調整。 diff --git a/content/docs/administration/state-machines.mdx b/content/docs/administration/state-machines.mdx index 07929e427..71702fc5f 100644 --- a/content/docs/administration/state-machines.mdx +++ b/content/docs/administration/state-machines.mdx @@ -5,14 +5,14 @@ description: How HotCRM declares valid status transitions on leads and opportuni # State Machines -Some objects have a strict **lifecycle** — a lead progresses from *New* to *Working* to *Qualified*, never jumping backwards into *New*. A state machine **declares** that route; it does not enforce it. All five machines in this app carry **warning** severity, so a user can move a record outside the route and **the save still goes through** — see *A declared route is advice, not a gate* below. +Some objects have a strict **lifecycle** — a lead progresses from *New* to *Contacted* to *Qualified* and on to *Converted*. A state machine **declares** that route; it does not enforce it. All five machines in this app carry **warning** severity, so a user can move a record outside the route and **the save still goes through** — see *A declared route is advice, not a gate* below. ## Where state machines apply HotCRM uses state machines on **five** objects: -- **Leads** — *New → Working → Qualified → Converted / Disqualified* -- **Opportunities** — through the 7 stages *Prospecting → Qualification → Proposal → Negotiation → Closed Won / Closed Lost / Closed Without Decision* +- **Leads** — *New → Contacted → Qualified → Converted*, with *Unqualified* reachable from every open status and the only way back out of it being a re-open to *New* +- **Opportunities** — through the 7 stages *Prospecting → Qualification → Needs Analysis → Proposal → Negotiation → Closed Won / Closed Lost* - **Cases** — *New → In Progress → Resolved → Closed*, with *Waiting on Customer* and *Waiting on Support* to park an open case, *Escalated* reachable from any unresolved status, and both *Resolved* and *Closed* able to reopen to *In Progress* - **Contracts** — *Draft → In Approval → Activated → Expired*, with *In Approval* able to send the paperwork back to *Draft*, *Terminated* reachable from all three live statuses, and *Expired* and *Terminated* both final - **Quotes** — *Draft → In Review → Presented → Accepted*, with *In Review* able to return to *Draft*, *Rejected* starting over at *Draft*, *Expired* reachable from *Draft*, *In Review* and *Presented*, and *Accepted* and *Expired* both final @@ -25,63 +25,75 @@ All five are declared the same way — a named `state_machine` validation rule o ## The Lead state machine +The main route, with the two side edges that leave it: + ``` - ┌──────────────┐ - │ New │ - └──────┬───────┘ - │ Begin working - ▼ - ┌──────────────┐ - │ Working │ - └──┬────────┬──┘ - Disqual │ │ Qualify - ▼ ▼ - ┌────────────┐ ┌────────────┐ - │Disqualified│ │ Qualified │ - └────────────┘ └─────┬──────┘ - │ Convert - ▼ - ┌────────────┐ - │ Converted │ - └────────────┘ + New ──Contact──▶ Contacted ──Qualify──▶ Qualified ──Convert──▶ Converted + ▲ │ │ (terminal) + │ │ Disqualify │ Disqualify + │ └───────────┬────────────┘ + │ ▼ + │ ┌─────────────┐ + └────────── Re-open ───────│ Unqualified │ + └─────────────┘ ``` -**Allowed transitions**: +**Allowed transitions** — this is the whole table, copied from the `lead_status_progression` rule in `src/objects/lead.object.ts`, and it carries a few shortcuts the picture above leaves out: -| From | To | Trigger | +| From | May move to | Notes | | --- | --- | --- | -| New | Working | Manual ("Begin Working") or auto-update on first activity | -| Working | Qualified | Manual ("Mark Qualified") | -| Working | Disqualified | Manual ("Disqualify") with required reason | -| Qualified | Converted | The Convert wizard (creates account/contact/opportunity) | -| Disqualified | (terminal) | — | -| Converted | (terminal) | — | +| New | Contacted, Qualified, Unqualified, Converted | A lead can be qualified or converted without ever passing through *Contacted* | +| Contacted | Qualified, Unqualified, Converted | | +| Qualified | Converted, Unqualified | | +| Unqualified | New | Re-open. This is the only edge out of *Unqualified* — it is not a terminal status | +| Converted | — | Terminal | + +Two of those edges have machinery behind them. Moving to *Unqualified* needs a **Disqualification Reason**: the field's `requiredWhen` fires on `status == "unqualified"` and that one **does** reject the save. Moving to *Converted* is the **Convert Lead** button, which runs the `lead_conversion` flow — it creates the account, contact and opportunity and stamps `status: 'converted'` at the end. + +**Allowed on purpose**: *New* directly to *Converted*. Convert Lead is visible on any lead that is neither converted nor unqualified — so *New*, *Contacted* and *Qualified* all show it — and the flow stamps the status itself, so that shortcut has to be in the table or every conversion from an unworked lead would log a spurious "invalid transition" warning. The comment above the table in `lead.object.ts` says exactly this. -**Disallowed**: jumping from *New* directly to *Converted*; reverting *Converted* back to *Working*. +**Not in the table**: anything out of *Converted*, which is terminal; stepping back from *Qualified* to *Contacted* or *New*; and re-qualifying an *Unqualified* lead in one move — the way back from there is a re-open to *New*. ## The Opportunity state machine ``` -Prospecting → Qualification → Proposal → Negotiation - │ - ┌─────────────────┼────────────────────────┐ - ▼ ▼ ▼ - Closed Won Closed Lost Closed Without Decision +Prospecting → Qualification → Needs Analysis → Proposal → Negotiation + │ │ + └─────┬──────┘ + ▼ + Closed Won + +any of the five open stages ─────────────────────▶ Closed Lost ``` -**Rules**: +There are **two** closed stages, not three: *Closed Won* and *Closed Lost*. There is no *Closed Without Decision* — a deal that goes quiet is a *Closed Lost*. + +**Allowed transitions**, copied from the `opportunity_stage_progression` rule in `src/objects/opportunity.object.ts`: -- You can move **forward** through *Prospecting → Qualification → Proposal → Negotiation* freely. -- You can move **backwards** (e.g., from *Negotiation* back to *Proposal*) — sometimes deals slip. -- From any open stage, you can move to one of the three **closed** stages. -- Once *Closed*, the opportunity is **locked** for amount/stage edits (except via reopen — admin only). +| From | May move to | Notes | +| --- | --- | --- | +| Prospecting | Qualification, Proposal, Closed Lost | | +| Qualification | Needs Analysis, Proposal, Closed Lost | | +| Needs Analysis | Proposal, Closed Lost | 40% default probability, *Best Case* forecast category | +| Proposal | Negotiation, Closed Won, Closed Lost | | +| Negotiation | Closed Won, Closed Lost | | +| Closed Won | — | Terminal | +| Closed Lost | — | Terminal | + +Two of those edges exist for the CPQ path rather than for a rep clicking through stages: *→ Proposal* is legal from every pre-proposal stage because the `quote_generation` flow fast-forwards the deal to *Proposal* the moment a quote is generated, which can happen at any open stage; and *Proposal → Closed Won* is legal because the `quote_on_accepted` hook wins the linked deal straight from an accepted quote, with no separate negotiation step. + +**Not in the table**: moving **backwards**. No open stage declares an edge to an earlier one, so *Negotiation* back to *Proposal* — a deal slipping — is outside the route and logs a warning like any other departure (it still saves, per the note above). Neither closed stage declares an edge either, so there is no reopen transition and no admin path back to an open stage. + +A closed opportunity is separately **frozen**, and by more than the stage field: the `opportunity_lifecycle` hook refuses any user edit to a *Closed Won* or *Closed Lost* record except to `description`, `next_step` and `notes`, and that refusal is a thrown error naming the fields attempted — not a warning. Approval verdicts and platform-managed columns are exempted so an in-flight approval can still land, and a write with no authenticated user (a seed or a backfill) is not guarded at all. The system also enforces: -- **Close Date is required** before moving to *Proposal*. -- **Amount is required** before moving to *Negotiation*. -- **Won/Lost reason is required** when entering *Closed Lost* or *Closed Won*. -- **Approval may be required** to enter *Closed Won* if amount > $500K and discount approval hasn't been granted. +- **Close Date is required** — and not from *Proposal* onwards, but always: `close_date` carries `required: true` with `notNull` storage, so an opportunity cannot be created without one. +- **Amount is required** — unconditional in the same way. Neither field waits for a stage. +- **Won/Lost reason is required** when entering *Closed Lost* or *Closed Won* — `requiredWhen` predicates on `win_reason` and `loss_reason`, keyed on the stage. +- **Approval starts at $100K.** The `opportunity_approval` flow fires on any update that leaves an open opportunity above **$100,000** with no approval on file, and routes it to a **Sales Manager**. **$500K** is the second tier, not the entry: above that a **Sales Director** signs off after the manager. So a $200K deal does go through approval. + +Unlike the transition tables, these four do reject the save: `required` and `requiredWhen` are hard, and while an approval step is pending the flow holds the record locked. ## What a state machine buys you @@ -115,13 +127,11 @@ All of that is a source edit, not a Setup screen: **there is no Setup → Object ## State machines and the AI Copilot -The Copilot understands the state machine. When suggesting an action: +**Nothing in this app connects the two.** The six skills under `src/skills/` and every action under `src/actions/` name no transition table, no state machine and no legal-next-state lookup, and no skill's instructions carry a list of which statuses may follow which. This page previously described the Copilot withholding a *"Qualify this lead"* or *"Convert this lead"* suggestion until the record's status allowed the move; HotCRM declares nothing that would produce that behaviour. -- *"Qualify this lead"* → only suggested if status is *Working*. -- *"Convert this lead"* → only suggested if status is *Qualified*. -- *"Mark this opportunity Closed Won"* → only suggested if win conditions are met (amount, close date, win reason, approval if required). +Whether the platform's own agent reads a `state_machine` validation when it loads an object's metadata, and narrows its suggestions accordingly, is a question about the runtime rather than about this app — so this page makes no claim about it in either direction. If you are relying on the Copilot to stay inside a lifecycle, verify it against your deployment rather than against this page. -This prevents the Copilot from suggesting illegal transitions. +What the tables do give an agent is what they give a report author: the route, declared in one place on the object, for a skill or a flow to be written against deliberately. ## Tips for admins diff --git a/content/docs/administration/state-machines.zh-Hans.mdx b/content/docs/administration/state-machines.zh-Hans.mdx index 2b6c68f94..70c7f20af 100644 --- a/content/docs/administration/state-machines.zh-Hans.mdx +++ b/content/docs/administration/state-machines.zh-Hans.mdx @@ -5,14 +5,14 @@ description: HotCRM 如何在潜在客户和商机上声明有效的状态转换 # 状态机 -某些对象有严格的 **生命周期** —— 潜在客户从 *New* 推进到 *Working* 再到 *Qualified*,绝不会倒退回 *New*。状态机把这条路线 **声明** 出来,但并不强制执行:本应用的五条状态机全是 **警告** 级,用户把记录挪出这条路线,**保存照样通过** —— 详见下文《声明出来的路线是建议,不是闸门》。 +某些对象有严格的 **生命周期** —— 潜在客户从 *New* 推进到 *Contacted*、*Qualified*,最后到 *Converted*。状态机把这条路线 **声明** 出来,但并不强制执行:本应用的五条状态机全是 **警告** 级,用户把记录挪出这条路线,**保存照样通过** —— 详见下文《声明出来的路线是建议,不是闸门》。 ## 状态机在哪里适用 HotCRM 在 **五个** 对象上使用状态机: -- **潜在客户** —— *New → Working → Qualified → Converted / Disqualified* -- **商机** —— 经过 7 个阶段 *Prospecting → Qualification → Proposal → Negotiation → Closed Won / Closed Lost / Closed Without Decision* +- **潜在客户** —— *New → Contacted → Qualified → Converted*;*Unqualified* 从任何开放状态都能进入,而它唯一的出路是重新打开回 *New* +- **商机** —— 经过 7 个阶段 *Prospecting → Qualification → Needs Analysis → Proposal → Negotiation → Closed Won / Closed Lost* - **案例** —— *New → In Progress → Resolved → Closed*;*Waiting on Customer* 与 *Waiting on Support* 用来把未结案的案例挂起,*Escalated* 从任何未解决的状态都能进入,*Resolved* 与 *Closed* 都可以重新打开回 *In Progress* - **合同** —— *Draft → In Approval → Activated → Expired*;*In Approval* 可以把文件退回 *Draft*,三种在途状态都能直接走到 *Terminated*,*Expired* 与 *Terminated* 都是终点 - **报价** —— *Draft → In Review → Presented → Accepted*;*In Review* 可以退回 *Draft*,*Rejected* 会回到 *Draft* 重来,*Expired* 可从 *Draft*、*In Review*、*Presented* 进入,*Accepted* 与 *Expired* 都是终点 @@ -25,63 +25,75 @@ HotCRM 在 **五个** 对象上使用状态机: ## 潜在客户状态机 +主路线,以及从它岔出去的两条边: + ``` - ┌──────────────┐ - │ New │ - └──────┬───────┘ - │ Begin working - ▼ - ┌──────────────┐ - │ Working │ - └──┬────────┬──┘ - Disqual │ │ Qualify - ▼ ▼ - ┌────────────┐ ┌────────────┐ - │Disqualified│ │ Qualified │ - └────────────┘ └─────┬──────┘ - │ Convert - ▼ - ┌────────────┐ - │ Converted │ - └────────────┘ + New ──Contact──▶ Contacted ──Qualify──▶ Qualified ──Convert──▶ Converted + ▲ │ │ (terminal) + │ │ Disqualify │ Disqualify + │ └───────────┬────────────┘ + │ ▼ + │ ┌─────────────┐ + └────────── Re-open ───────│ Unqualified │ + └─────────────┘ ``` -**允许的转换**: +**允许的转换** —— 下面就是整张表,照抄自 `src/objects/lead.object.ts` 里的 `lead_status_progression` 规则;它比上面那张图多带几条捷径: -| 从 | 到 | 触发 | +| 从 | 可以移动到 | 说明 | | --- | --- | --- | -| New | Working | 手动(“Begin Working”)或首次活动时自动更新 | -| Working | Qualified | 手动(“Mark Qualified”) | -| Working | Disqualified | 手动(“Disqualify”),需填写原因 | -| Qualified | Converted | 转化向导(创建客户/联系人/商机) | -| Disqualified | (终态) | — | -| Converted | (终态) | — | +| New | Contacted、Qualified、Unqualified、Converted | 潜在客户可以不经过 *Contacted* 就被确认资格或被转化 | +| Contacted | Qualified、Unqualified、Converted | | +| Qualified | Converted、Unqualified | | +| Unqualified | New | 重新打开。这是 *Unqualified* 唯一的出边 —— 它不是终态 | +| Converted | — | 终态 | + +其中两条边背后是有机制的。移动到 *Unqualified* 必须填写 **取消资格原因**:该字段的 `requiredWhen` 在 `status == "unqualified"` 时生效,而这一条 **确实** 会拒绝保存。移动到 *Converted* 走的是 **Convert Lead** 按钮,它运行 `lead_conversion` 流程 —— 流程创建客户、联系人和商机,最后盖上 `status: 'converted'`。 + +**这是有意允许的**:从 *New* 直接到 *Converted*。Convert Lead 按钮在任何既未转化也未被取消资格的线索上都可见 —— 也就是 *New*、*Contacted*、*Qualified* 三种状态都会显示它 —— 而流程自己会盖状态,所以这条捷径必须写进表里,否则每一次从未跟进过的线索转化都会记下一条名不副实的「非法转换」警告。`lead.object.ts` 里表上方的注释说的正是这件事。 -**不允许**:从 *New* 直接跳到 *Converted*;将 *Converted* 退回到 *Working*。 +**表里没有的**:*Converted* 的任何出边,它是终态;从 *Qualified* 退回 *Contacted* 或 *New*;以及把 *Unqualified* 一步重新确认资格 —— 从那里回来的路是重新打开到 *New*。 ## 商机状态机 ``` -Prospecting → Qualification → Proposal → Negotiation - │ - ┌─────────────────┼────────────────────────┐ - ▼ ▼ ▼ - Closed Won Closed Lost Closed Without Decision +Prospecting → Qualification → Needs Analysis → Proposal → Negotiation + │ │ + └─────┬──────┘ + ▼ + Closed Won + +any of the five open stages ─────────────────────▶ Closed Lost ``` -**规则**: +已关闭阶段只有 **两个**,不是三个:*Closed Won* 与 *Closed Lost*。不存在 *Closed Without Decision* —— 一笔无声无息拖没了的交易就是 *Closed Lost*。 + +**允许的转换**,照抄自 `src/objects/opportunity.object.ts` 里的 `opportunity_stage_progression` 规则: -- 你可以自由地 **向前** 经过 *Prospecting → Qualification → Proposal → Negotiation*。 -- 你可以 **向后** 移动(例如从 *Negotiation* 退回 *Proposal*)—— 有时交易会滑落。 -- 从任何开放阶段,你都可以移动到三个 **已关闭** 阶段之一。 -- 一旦 *Closed*,商机的金额/阶段编辑就被 **锁定**(除非重新打开 —— 仅限管理员)。 +| 从 | 可以移动到 | 说明 | +| --- | --- | --- | +| Prospecting | Qualification、Proposal、Closed Lost | | +| Qualification | Needs Analysis、Proposal、Closed Lost | | +| Needs Analysis | Proposal、Closed Lost | 默认赢率 40%,预测类别为 *Best Case* | +| Proposal | Negotiation、Closed Won、Closed Lost | | +| Negotiation | Closed Won、Closed Lost | | +| Closed Won | — | 终态 | +| Closed Lost | — | 终态 | + +其中两条边是为 CPQ 路径存在的,不是给销售一个阶段一个阶段点出来的:*→ Proposal* 从每一个 Proposal 之前的阶段都合法,因为 `quote_generation` 流程一旦生成报价就把交易快进到 *Proposal*,而生成报价在任何开放阶段都可能发生;*Proposal → Closed Won* 合法,则是因为 `quote_on_accepted` 钩子会从一份被接受的报价上直接赢下关联交易,中间不再经过一次谈判。 + +**表里没有的**:**向后** 移动。没有任何一个开放阶段声明了通往更早阶段的边,所以从 *Negotiation* 退回 *Proposal* —— 交易滑落 —— 是走出了这条路线,和其他任何一次出格一样只记一条警告(照样保存,见上文)。两个已关闭阶段也都没有出边,所以既不存在「重新打开」这条转换,也不存在管理员绕回开放阶段的通道。 + +已关闭的商机另有一道 **冻结**,而且管的远不止阶段字段:`opportunity_lifecycle` 钩子会拒绝用户对 *Closed Won* / *Closed Lost* 记录的任何编辑,只放行 `description`、`next_step` 和 `notes` 三个字段;而且这次拒绝是抛出错误、并点名你试图改的那些字段 —— 不是警告。审批结论和平台托管的列被排除在外,好让一次在途审批仍能落地;而没有登录用户的写入(种子数据或回填)根本不受这道冻结管辖。 系统还强制执行: -- 移动到 *Proposal* 之前 **必须填写关闭日期**。 -- 移动到 *Negotiation* 之前 **必须填写金额**。 -- 进入 *Closed Lost* 或 *Closed Won* 时 **必须填写赢单/丢单原因**。 -- 如果金额 > $500K 且尚未授予折扣审批,进入 *Closed Won* **可能需要审批**。 +- **必须填写关闭日期** —— 而且不是从 *Proposal* 开始才要求,是一直要求:`close_date` 带着 `required: true` 和 `notNull` 存储,没有它商机根本建不出来。 +- **必须填写金额** —— 同样是无条件的。这两个字段都不等哪个阶段。 +- 进入 *Closed Lost* 或 *Closed Won* 时 **必须填写赢单/丢单原因** —— 这是 `win_reason` 与 `loss_reason` 上按阶段生效的 `requiredWhen` 谓词。 +- **审批的入口是 $100K。** `opportunity_approval` 流程会在任何一次更新之后触发,只要这笔开放商机金额超过 **$100,000** 且还没有在案的审批,就把它送到 **销售经理** 那里。**$500K** 是第二档而不是入口:超过这个数,还要 **销售总监** 在经理之后加签。所以一笔 $200K 的单子是会进审批的。 + +与上面那些转换表不同,这四条确实会拒绝保存:`required` 与 `requiredWhen` 是硬的,而在审批步骤挂起期间,流程会把记录锁住。 ## 状态机给你的是什么 @@ -115,13 +127,11 @@ Prospecting → Qualification → Proposal → Negotiation ## 状态机与 AI Copilot -Copilot 理解状态机。在建议操作时: +**本应用没有把这两者接起来。** `src/skills/` 下的六个技能、`src/actions/` 下的每一个操作,都没有提到转换表、状态机或「合法的下一批状态」这类查询,也没有任何一个技能的提示词里写着哪个状态之后可以接哪个状态。本页原先描述过 Copilot 会压住 *“确认这个潜在客户的资格”*、*“转化这个潜在客户”* 这类建议,直到记录的状态允许这次移动为止;HotCRM 并没有声明任何能产生这种行为的东西。 -- *“确认这个潜在客户的资格”* → 仅当状态为 *Working* 时建议。 -- *“转化这个潜在客户”* → 仅当状态为 *Qualified* 时建议。 -- *“将这个商机标记为 Closed Won”* → 仅当赢单条件满足时建议(金额、关闭日期、赢单原因、必要时的审批)。 +平台自己的 agent 在装载对象元数据时会不会把 `state_machine` 验证一并读进去、并据此收窄建议,这是运行时的问题,不是本应用的问题 —— 所以本页对它不作任何方向的断言。如果你正指望 Copilot 待在某条生命周期里,请在你自己的部署上验证,而不是照着本页相信。 -这可以防止 Copilot 建议非法的转换。 +转换表能给 agent 的,和它给报表作者的是同一样东西:那条路线,声明在对象上的一个地方,供技能或流程有意识地照着写。 ## 给管理员的提示 diff --git a/content/docs/administration/state-machines.zh-Hant.mdx b/content/docs/administration/state-machines.zh-Hant.mdx index acd803a57..deb3bf0d7 100644 --- a/content/docs/administration/state-machines.zh-Hant.mdx +++ b/content/docs/administration/state-machines.zh-Hant.mdx @@ -5,14 +5,14 @@ description: HotCRM 如何在潛在客戶和商機上宣告有效的狀態轉換 # 狀態機 -某些物件有嚴格的 **生命週期** —— 潛在客戶從 *New* 推進到 *Working* 再到 *Qualified*,絕不會倒退回 *New*。狀態機把這條路線 **宣告** 出來,但並不強制執行:本應用的五條狀態機全是 **警告** 級,使用者把記錄挪出這條路線,**儲存照樣通過** —— 詳見下文《宣告出來的路線是建議,不是閘門》。 +某些物件有嚴格的 **生命週期** —— 潛在客戶從 *New* 推進到 *Contacted*、*Qualified*,最後到 *Converted*。狀態機把這條路線 **宣告** 出來,但並不強制執行:本應用的五條狀態機全是 **警告** 級,使用者把記錄挪出這條路線,**儲存照樣通過** —— 詳見下文《宣告出來的路線是建議,不是閘門》。 ## 狀態機在哪裡適用 HotCRM 在 **五個** 物件上使用狀態機: -- **潛在客戶** —— *New → Working → Qualified → Converted / Disqualified* -- **商機** —— 經過 7 個階段 *Prospecting → Qualification → Proposal → Negotiation → Closed Won / Closed Lost / Closed Without Decision* +- **潛在客戶** —— *New → Contacted → Qualified → Converted*;*Unqualified* 從任何開放狀態都能進入,而它唯一的出路是重新開啟回 *New* +- **商機** —— 經過 7 個階段 *Prospecting → Qualification → Needs Analysis → Proposal → Negotiation → Closed Won / Closed Lost* - **案例** —— *New → In Progress → Resolved → Closed*;*Waiting on Customer* 與 *Waiting on Support* 用來把未結案的案例掛起,*Escalated* 從任何未解決的狀態都能進入,*Resolved* 與 *Closed* 都可以重新開啟回 *In Progress* - **合約** —— *Draft → In Approval → Activated → Expired*;*In Approval* 可以把文件退回 *Draft*,三種在途狀態都能直接走到 *Terminated*,*Expired* 與 *Terminated* 都是終點 - **報價** —— *Draft → In Review → Presented → Accepted*;*In Review* 可以退回 *Draft*,*Rejected* 會回到 *Draft* 重來,*Expired* 可從 *Draft*、*In Review*、*Presented* 進入,*Accepted* 與 *Expired* 都是終點 @@ -25,63 +25,75 @@ HotCRM 在 **五個** 物件上使用狀態機: ## 潛在客戶狀態機 +主路線,以及從它岔出去的兩條邊: + ``` - ┌──────────────┐ - │ New │ - └──────┬───────┘ - │ Begin working - ▼ - ┌──────────────┐ - │ Working │ - └──┬────────┬──┘ - Disqual │ │ Qualify - ▼ ▼ - ┌────────────┐ ┌────────────┐ - │Disqualified│ │ Qualified │ - └────────────┘ └─────┬──────┘ - │ Convert - ▼ - ┌────────────┐ - │ Converted │ - └────────────┘ + New ──Contact──▶ Contacted ──Qualify──▶ Qualified ──Convert──▶ Converted + ▲ │ │ (terminal) + │ │ Disqualify │ Disqualify + │ └───────────┬────────────┘ + │ ▼ + │ ┌─────────────┐ + └────────── Re-open ───────│ Unqualified │ + └─────────────┘ ``` -**允許的轉換**: +**允許的轉換** —— 下面就是整張表,照抄自 `src/objects/lead.object.ts` 裡的 `lead_status_progression` 規則;它比上面那張圖多帶幾條捷徑: -| 從 | 到 | 觸發 | +| 從 | 可以移動到 | 說明 | | --- | --- | --- | -| New | Working | 手動(「Begin Working」)或首次活動時自動更新 | -| Working | Qualified | 手動(「Mark Qualified」) | -| Working | Disqualified | 手動(「Disqualify」),需填寫原因 | -| Qualified | Converted | 轉化精靈(建立客戶/聯絡人/商機) | -| Disqualified | (終態) | — | -| Converted | (終態) | — | +| New | Contacted、Qualified、Unqualified、Converted | 潛在客戶可以不經過 *Contacted* 就被確認資格或被轉化 | +| Contacted | Qualified、Unqualified、Converted | | +| Qualified | Converted、Unqualified | | +| Unqualified | New | 重新開啟。這是 *Unqualified* 唯一的出邊 —— 它不是終態 | +| Converted | — | 終態 | + +其中兩條邊背後是有機制的。移動到 *Unqualified* 必須填寫 **取消資格原因**:該欄位的 `requiredWhen` 在 `status == "unqualified"` 時生效,而這一條 **確實** 會拒絕儲存。移動到 *Converted* 走的是 **Convert Lead** 按鈕,它執行 `lead_conversion` 流程 —— 流程建立客戶、聯絡人和商機,最後蓋上 `status: 'converted'`。 + +**這是有意允許的**:從 *New* 直接到 *Converted*。Convert Lead 按鈕在任何既未轉化也未被取消資格的線索上都可見 —— 也就是 *New*、*Contacted*、*Qualified* 三種狀態都會顯示它 —— 而流程自己會蓋狀態,所以這條捷徑必須寫進表裡,否則每一次從未跟進過的線索轉化都會記下一條名不副實的「非法轉換」警告。`lead.object.ts` 裡表上方的註解說的正是這件事。 -**不允許**:從 *New* 直接跳到 *Converted*;將 *Converted* 退回到 *Working*。 +**表裡沒有的**:*Converted* 的任何出邊,它是終態;從 *Qualified* 退回 *Contacted* 或 *New*;以及把 *Unqualified* 一步重新確認資格 —— 從那裡回來的路是重新開啟到 *New*。 ## 商機狀態機 ``` -Prospecting → Qualification → Proposal → Negotiation - │ - ┌─────────────────┼────────────────────────┐ - ▼ ▼ ▼ - Closed Won Closed Lost Closed Without Decision +Prospecting → Qualification → Needs Analysis → Proposal → Negotiation + │ │ + └─────┬──────┘ + ▼ + Closed Won + +any of the five open stages ─────────────────────▶ Closed Lost ``` -**規則**: +已關閉階段只有 **兩個**,不是三個:*Closed Won* 與 *Closed Lost*。不存在 *Closed Without Decision* —— 一筆無聲無息拖沒了的交易就是 *Closed Lost*。 + +**允許的轉換**,照抄自 `src/objects/opportunity.object.ts` 裡的 `opportunity_stage_progression` 規則: -- 你可以自由地 **向前** 經過 *Prospecting → Qualification → Proposal → Negotiation*。 -- 你可以 **向後** 移動(例如從 *Negotiation* 退回 *Proposal*)—— 有時交易會滑落。 -- 從任何開放階段,你都可以移動到三個 **已關閉** 階段之一。 -- 一旦 *Closed*,商機的金額/階段編輯就被 **鎖定**(除非重新開啟 —— 僅限管理員)。 +| 從 | 可以移動到 | 說明 | +| --- | --- | --- | +| Prospecting | Qualification、Proposal、Closed Lost | | +| Qualification | Needs Analysis、Proposal、Closed Lost | | +| Needs Analysis | Proposal、Closed Lost | 預設贏率 40%,預測類別為 *Best Case* | +| Proposal | Negotiation、Closed Won、Closed Lost | | +| Negotiation | Closed Won、Closed Lost | | +| Closed Won | — | 終態 | +| Closed Lost | — | 終態 | + +其中兩條邊是為 CPQ 路徑存在的,不是給業務一個階段一個階段點出來的:*→ Proposal* 從每一個 Proposal 之前的階段都合法,因為 `quote_generation` 流程一旦產生報價就把交易快進到 *Proposal*,而產生報價在任何開放階段都可能發生;*Proposal → Closed Won* 合法,則是因為 `quote_on_accepted` 鉤子會從一份被接受的報價上直接贏下關聯交易,中間不再經過一次談判。 + +**表裡沒有的**:**向後** 移動。沒有任何一個開放階段宣告了通往更早階段的邊,所以從 *Negotiation* 退回 *Proposal* —— 交易滑落 —— 是走出了這條路線,和其他任何一次出格一樣只記一條警告(照樣儲存,見上文)。兩個已關閉階段也都沒有出邊,所以既不存在「重新開啟」這條轉換,也不存在管理員繞回開放階段的通道。 + +已關閉的商機另有一道 **凍結**,而且管的遠不止階段欄位:`opportunity_lifecycle` 鉤子會拒絕使用者對 *Closed Won* / *Closed Lost* 記錄的任何編輯,只放行 `description`、`next_step` 和 `notes` 三個欄位;而且這次拒絕是拋出錯誤、並點名你試圖改的那些欄位 —— 不是警告。審批結論和平台託管的欄位被排除在外,好讓一次在途審批仍能落地;而沒有登入使用者的寫入(種子資料或回填)根本不受這道凍結管轄。 系統還強制執行: -- 移動到 *Proposal* 之前 **必須填寫關閉日期**。 -- 移動到 *Negotiation* 之前 **必須填寫金額**。 -- 進入 *Closed Lost* 或 *Closed Won* 時 **必須填寫贏單/丟單原因**。 -- 如果金額 > $500K 且尚未授予折扣審批,進入 *Closed Won* **可能需要審批**。 +- **必須填寫關閉日期** —— 而且不是從 *Proposal* 開始才要求,是一直要求:`close_date` 帶著 `required: true` 和 `notNull` 儲存,沒有它商機根本建不出來。 +- **必須填寫金額** —— 同樣是無條件的。這兩個欄位都不等哪個階段。 +- 進入 *Closed Lost* 或 *Closed Won* 時 **必須填寫贏單/丟單原因** —— 這是 `win_reason` 與 `loss_reason` 上按階段生效的 `requiredWhen` 述詞。 +- **審批的入口是 $100K。** `opportunity_approval` 流程會在任何一次更新之後觸發,只要這筆開放商機金額超過 **$100,000** 且還沒有在案的審批,就把它送到 **銷售經理** 那裡。**$500K** 是第二檔而不是入口:超過這個數,還要 **銷售總監** 在經理之後加簽。所以一筆 $200K 的單子是會進審批的。 + +與上面那些轉換表不同,這四條確實會拒絕儲存:`required` 與 `requiredWhen` 是硬的,而在審批步驟擱置期間,流程會把記錄鎖住。 ## 狀態機給你的是什麼 @@ -115,13 +127,11 @@ Prospecting → Qualification → Proposal → Negotiation ## 狀態機與 AI Copilot -Copilot 理解狀態機。在建議操作時: +**本應用沒有把這兩者接起來。** `src/skills/` 下的六個技能、`src/actions/` 下的每一個操作,都沒有提到轉換表、狀態機或「合法的下一批狀態」這類查詢,也沒有任何一個技能的提示詞裡寫著哪個狀態之後可以接哪個狀態。本頁原先描述過 Copilot 會壓住 *「確認這個潛在客戶的資格」*、*「轉化這個潛在客戶」* 這類建議,直到記錄的狀態允許這次移動為止;HotCRM 並沒有宣告任何能產生這種行為的東西。 -- *「確認這個潛在客戶的資格」* → 僅當狀態為 *Working* 時建議。 -- *「轉化這個潛在客戶」* → 僅當狀態為 *Qualified* 時建議。 -- *「將這個商機標記為 Closed Won」* → 僅當贏單條件滿足時建議(金額、關閉日期、贏單原因、必要時的審批)。 +平台自己的 agent 在裝載物件中繼資料時會不會把 `state_machine` 驗證一併讀進去、並據此收窄建議,這是執行階段的問題,不是本應用的問題 —— 所以本頁對它不作任何方向的斷言。如果你正指望 Copilot 待在某條生命週期裡,請在你自己的部署上驗證,而不是照著本頁相信。 -這可以防止 Copilot 建議非法的轉換。 +轉換表能給 agent 的,和它給報表作者的是同一樣東西:那條路線,宣告在物件上的一個地方,供技能或流程有意識地照著寫。 ## 給管理員的提示 From 9c13a26b6cc5d93e75da55e4bb6e58b774325e40 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 12:59:41 +0000 Subject: [PATCH 2/3] docs(administration): name the real mechanism behind two enforcement claims Self-review of the previous commit caught two of my own sentences claiming more than src supports: - The disqualification-reason requirement is a separate error-severity validations[] rule named disqualification_reason_required, not a requiredWhen predicate on the field. (The enforcement itself is real - severity is 'error' - only the mechanism was mis-named.) - 'These four reject the save' was too broad: opportunity_approval is a record_change flow that runs after the update and locks the record, not a save-time check. Only the three required/requiredWhen claims reject a save. All three locales. --- content/docs/administration/state-machines.mdx | 4 ++-- content/docs/administration/state-machines.zh-Hans.mdx | 4 ++-- content/docs/administration/state-machines.zh-Hant.mdx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/content/docs/administration/state-machines.mdx b/content/docs/administration/state-machines.mdx index 71702fc5f..876296693 100644 --- a/content/docs/administration/state-machines.mdx +++ b/content/docs/administration/state-machines.mdx @@ -48,7 +48,7 @@ The main route, with the two side edges that leave it: | Unqualified | New | Re-open. This is the only edge out of *Unqualified* — it is not a terminal status | | Converted | — | Terminal | -Two of those edges have machinery behind them. Moving to *Unqualified* needs a **Disqualification Reason**: the field's `requiredWhen` fires on `status == "unqualified"` and that one **does** reject the save. Moving to *Converted* is the **Convert Lead** button, which runs the `lead_conversion` flow — it creates the account, contact and opportunity and stamps `status: 'converted'` at the end. +Two of those edges have machinery behind them. Moving to *Unqualified* needs a **Disqualification Reason**: a separate validation rule, `disqualification_reason_required`, fires when the status is `unqualified` and the reason is blank — and unlike the transition table it is `error` severity, so that one **does** reject the save. Moving to *Converted* is the **Convert Lead** button, which runs the `lead_conversion` flow — it creates the account, contact and opportunity and stamps `status: 'converted'` at the end. **Allowed on purpose**: *New* directly to *Converted*. Convert Lead is visible on any lead that is neither converted nor unqualified — so *New*, *Contacted* and *Qualified* all show it — and the flow stamps the status itself, so that shortcut has to be in the table or every conversion from an unworked lead would log a spurious "invalid transition" warning. The comment above the table in `lead.object.ts` says exactly this. @@ -93,7 +93,7 @@ The system also enforces: - **Won/Lost reason is required** when entering *Closed Lost* or *Closed Won* — `requiredWhen` predicates on `win_reason` and `loss_reason`, keyed on the stage. - **Approval starts at $100K.** The `opportunity_approval` flow fires on any update that leaves an open opportunity above **$100,000** with no approval on file, and routes it to a **Sales Manager**. **$500K** is the second tier, not the entry: above that a **Sales Director** signs off after the manager. So a $200K deal does go through approval. -Unlike the transition tables, these four do reject the save: `required` and `requiredWhen` are hard, and while an approval step is pending the flow holds the record locked. +The heading is fair for the first three, unlike the transition tables above them: `required` and `requiredWhen` are hard, and a save that violates one is refused. The fourth is not a save-time check at all — `opportunity_approval` is a `record_change` flow that runs *after* the update and then holds the record locked while a step is pending. ## What a state machine buys you diff --git a/content/docs/administration/state-machines.zh-Hans.mdx b/content/docs/administration/state-machines.zh-Hans.mdx index 70c7f20af..f06c5aa92 100644 --- a/content/docs/administration/state-machines.zh-Hans.mdx +++ b/content/docs/administration/state-machines.zh-Hans.mdx @@ -48,7 +48,7 @@ HotCRM 在 **五个** 对象上使用状态机: | Unqualified | New | 重新打开。这是 *Unqualified* 唯一的出边 —— 它不是终态 | | Converted | — | 终态 | -其中两条边背后是有机制的。移动到 *Unqualified* 必须填写 **取消资格原因**:该字段的 `requiredWhen` 在 `status == "unqualified"` 时生效,而这一条 **确实** 会拒绝保存。移动到 *Converted* 走的是 **Convert Lead** 按钮,它运行 `lead_conversion` 流程 —— 流程创建客户、联系人和商机,最后盖上 `status: 'converted'`。 +其中两条边背后是有机制的。移动到 *Unqualified* 必须填写 **取消资格原因**:这是另一条名为 `disqualification_reason_required` 的验证规则,在状态为 `unqualified` 而原因为空时触发 —— 而且与转换表不同,它是 `error` 级,所以这一条 **确实** 会拒绝保存。移动到 *Converted* 走的是 **Convert Lead** 按钮,它运行 `lead_conversion` 流程 —— 流程创建客户、联系人和商机,最后盖上 `status: 'converted'`。 **这是有意允许的**:从 *New* 直接到 *Converted*。Convert Lead 按钮在任何既未转化也未被取消资格的线索上都可见 —— 也就是 *New*、*Contacted*、*Qualified* 三种状态都会显示它 —— 而流程自己会盖状态,所以这条捷径必须写进表里,否则每一次从未跟进过的线索转化都会记下一条名不副实的「非法转换」警告。`lead.object.ts` 里表上方的注释说的正是这件事。 @@ -93,7 +93,7 @@ any of the five open stages ───────────────── - 进入 *Closed Lost* 或 *Closed Won* 时 **必须填写赢单/丢单原因** —— 这是 `win_reason` 与 `loss_reason` 上按阶段生效的 `requiredWhen` 谓词。 - **审批的入口是 $100K。** `opportunity_approval` 流程会在任何一次更新之后触发,只要这笔开放商机金额超过 **$100,000** 且还没有在案的审批,就把它送到 **销售经理** 那里。**$500K** 是第二档而不是入口:超过这个数,还要 **销售总监** 在经理之后加签。所以一笔 $200K 的单子是会进审批的。 -与上面那些转换表不同,这四条确实会拒绝保存:`required` 与 `requiredWhen` 是硬的,而在审批步骤挂起期间,流程会把记录锁住。 +对前三条来说,「强制执行」这个说法是站得住的 —— 与它们上面那些转换表不同:`required` 与 `requiredWhen` 是硬的,违反它们的保存会被拒绝。第四条则根本不是保存时的检查 —— `opportunity_approval` 是一条 `record_change` 流程,在更新**之后**才跑,然后在审批步骤挂起期间把记录锁住。 ## 状态机给你的是什么 diff --git a/content/docs/administration/state-machines.zh-Hant.mdx b/content/docs/administration/state-machines.zh-Hant.mdx index deb3bf0d7..fbbe78a91 100644 --- a/content/docs/administration/state-machines.zh-Hant.mdx +++ b/content/docs/administration/state-machines.zh-Hant.mdx @@ -48,7 +48,7 @@ HotCRM 在 **五個** 物件上使用狀態機: | Unqualified | New | 重新開啟。這是 *Unqualified* 唯一的出邊 —— 它不是終態 | | Converted | — | 終態 | -其中兩條邊背後是有機制的。移動到 *Unqualified* 必須填寫 **取消資格原因**:該欄位的 `requiredWhen` 在 `status == "unqualified"` 時生效,而這一條 **確實** 會拒絕儲存。移動到 *Converted* 走的是 **Convert Lead** 按鈕,它執行 `lead_conversion` 流程 —— 流程建立客戶、聯絡人和商機,最後蓋上 `status: 'converted'`。 +其中兩條邊背後是有機制的。移動到 *Unqualified* 必須填寫 **取消資格原因**:這是另一條名為 `disqualification_reason_required` 的驗證規則,在狀態為 `unqualified` 而原因為空時觸發 —— 而且與轉換表不同,它是 `error` 級,所以這一條 **確實** 會拒絕儲存。移動到 *Converted* 走的是 **Convert Lead** 按鈕,它執行 `lead_conversion` 流程 —— 流程建立客戶、聯絡人和商機,最後蓋上 `status: 'converted'`。 **這是有意允許的**:從 *New* 直接到 *Converted*。Convert Lead 按鈕在任何既未轉化也未被取消資格的線索上都可見 —— 也就是 *New*、*Contacted*、*Qualified* 三種狀態都會顯示它 —— 而流程自己會蓋狀態,所以這條捷徑必須寫進表裡,否則每一次從未跟進過的線索轉化都會記下一條名不副實的「非法轉換」警告。`lead.object.ts` 裡表上方的註解說的正是這件事。 @@ -93,7 +93,7 @@ any of the five open stages ───────────────── - 進入 *Closed Lost* 或 *Closed Won* 時 **必須填寫贏單/丟單原因** —— 這是 `win_reason` 與 `loss_reason` 上按階段生效的 `requiredWhen` 述詞。 - **審批的入口是 $100K。** `opportunity_approval` 流程會在任何一次更新之後觸發,只要這筆開放商機金額超過 **$100,000** 且還沒有在案的審批,就把它送到 **銷售經理** 那裡。**$500K** 是第二檔而不是入口:超過這個數,還要 **銷售總監** 在經理之後加簽。所以一筆 $200K 的單子是會進審批的。 -與上面那些轉換表不同,這四條確實會拒絕儲存:`required` 與 `requiredWhen` 是硬的,而在審批步驟擱置期間,流程會把記錄鎖住。 +對前三條來說,「強制執行」這個說法是站得住的 —— 與它們上面那些轉換表不同:`required` 與 `requiredWhen` 是硬的,違反它們的儲存會被拒絕。第四條則根本不是儲存時的檢查 —— `opportunity_approval` 是一條 `record_change` 流程,在更新**之後**才跑,然後在審批步驟擱置期間把記錄鎖住。 ## 狀態機給你的是什麼 From 114b903ccd6371220e34d9a773dba7b6aecfa08f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 13:01:34 +0000 Subject: [PATCH 3/3] docs(administration): keep the opportunity diagram label-only in all three locales The Closed Lost line in the diagram was an English sentence ('any of the five open stages'), which reads as untranslated text on the two Chinese pages while CJK replacements would break monospace alignment. Replaced with the stage labels themselves, which are already English in these diagrams by design and carry the same fact. --- content/docs/administration/state-machines.mdx | 2 +- content/docs/administration/state-machines.zh-Hans.mdx | 2 +- content/docs/administration/state-machines.zh-Hant.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/content/docs/administration/state-machines.mdx b/content/docs/administration/state-machines.mdx index 876296693..990d02d89 100644 --- a/content/docs/administration/state-machines.mdx +++ b/content/docs/administration/state-machines.mdx @@ -63,7 +63,7 @@ Prospecting → Qualification → Needs Analysis → Proposal → Negotiation ▼ Closed Won -any of the five open stages ─────────────────────▶ Closed Lost +Prospecting / Qualification / Needs Analysis / Proposal / Negotiation ──▶ Closed Lost ``` There are **two** closed stages, not three: *Closed Won* and *Closed Lost*. There is no *Closed Without Decision* — a deal that goes quiet is a *Closed Lost*. diff --git a/content/docs/administration/state-machines.zh-Hans.mdx b/content/docs/administration/state-machines.zh-Hans.mdx index f06c5aa92..11e34cb2c 100644 --- a/content/docs/administration/state-machines.zh-Hans.mdx +++ b/content/docs/administration/state-machines.zh-Hans.mdx @@ -63,7 +63,7 @@ Prospecting → Qualification → Needs Analysis → Proposal → Negotiation ▼ Closed Won -any of the five open stages ─────────────────────▶ Closed Lost +Prospecting / Qualification / Needs Analysis / Proposal / Negotiation ──▶ Closed Lost ``` 已关闭阶段只有 **两个**,不是三个:*Closed Won* 与 *Closed Lost*。不存在 *Closed Without Decision* —— 一笔无声无息拖没了的交易就是 *Closed Lost*。 diff --git a/content/docs/administration/state-machines.zh-Hant.mdx b/content/docs/administration/state-machines.zh-Hant.mdx index fbbe78a91..223bd64a6 100644 --- a/content/docs/administration/state-machines.zh-Hant.mdx +++ b/content/docs/administration/state-machines.zh-Hant.mdx @@ -63,7 +63,7 @@ Prospecting → Qualification → Needs Analysis → Proposal → Negotiation ▼ Closed Won -any of the five open stages ─────────────────────▶ Closed Lost +Prospecting / Qualification / Needs Analysis / Proposal / Negotiation ──▶ Closed Lost ``` 已關閉階段只有 **兩個**,不是三個:*Closed Won* 與 *Closed Lost*。不存在 *Closed Without Decision* —— 一筆無聲無息拖沒了的交易就是 *Closed Lost*。