Skip to content

feat(forecast): add the forecast_snapshot nightly writer (#590) - #615

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-590-forecast-snapshot
Aug 2, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-590-forecast-snapshot

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #590

Description

crm_forecast 的文档承诺了一个 nightly 快照任务,但这个任务从来不存在:forecast.hook.ts 只做字段派生,src/ 里没有任何 flow / hook / job 创建过 crm_forecast 行。结果是整条预测链路——forecast 对象、attainment_pct / coverage_ratio 公式、forecast_metrics 数据集、仪表盘的配额达成表——全部跑在 src/data/index.ts 里手写的 3 条种子数据上。在任何真实环境里,这个功能都是一个空壳。

本 PR 新增 forecast_snapshot 调度 flow(每晚 03:00,runAs: 'system'),为每一位活跃商机负责人写入一条当季快照。

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Documentation update

Related Issues

Fixes #590
Related to #530 (期间边界必须是日历对齐的), #567 (loop 内条件必须是显式 CEL envelope)

发现但未在本 PR 修复的问题,已另开 unassigned issue:#613(forecast 种子的 externalId: 'period_label' 不再是唯一键)、#614(quota_attainment_by_rep 跨期间求和导致重复计数)。

Changes Made

  • src/flows/forecast-snapshot.flow.ts(新增) — 每晚 03:00 扫描:遍历用户 → 用一次 findOne 判断其是否拥有非 closed_lost 商机(不是负责人就完全跳过,不留空行)→ 定位/创建"窗口包含今天"的当季快照行 → 重新读回该行 → 用 4 组 get_record + loop 把金额累加进去 → update_record 写回。
  • 金额口径:按存储列 forecast_category 分桶,且是累进的(pipeline ⊇ best_case ⊇ commit)。这与对象自身字段的语义(pipeline 是"所有开放商机")、种子数据的量级关系、以及已有的 closing_this_quarter 视图(把 ['commit', 'best_case'] 当作一个桶)三者一致。改用 probability 阈值会凭空造出第二套 "commit" 定义,和列表视图、pipeline_by_forecast_category 图表悄悄打架。
  • quota 永不写入 —— 它是 attainment_pct 的分母,在配额模型出现之前由人工维护。
  • src/objects/forecast.hook.ts — 现在除了 period_end / period_label,也派生 period_start。flow 模板只能解析 {TODAY()} 和天偏移,无法表达"本日历季度的第一天";把这个边界交给对象派生,等于让 [CRM] Forecast「This Quarter」视图恒为空:种子数据 period_start 不落在季度边界 #530 为种子数据钉住的日历对齐不变式,对运行期快照同样成立。这也是"声明即强制":调用方只要给出 period,就不可能写出一个边界歪掉的快照。
  • src/objects/forecast.object.ts — 文档注释指向真实的 writer 与调度;best_case_amount / commit_amount 的描述改为它们真正度量的东西(存储的 forecast category),原先写的概率阈值没有任何 writer 执行,而且和 stage → category 映射本身就矛盾(proposal 是 60% 却落在 commit)。
  • test/helpers/flow-harness.ts — 新增可选的 hooks 参数(默认关闭,不影响既有测试),让 runtime 测试能把 flow 和 hook 放在一起验证。forecast_snapshot 只交给 crm_forecast 一个 period,如果不跑 hook,测试会"通过",却写出一条没有任何消费方能查到的行。
  • 文档 — content/docs/administration/automation.mdx 补上 flow 表格行与计数;content/docs/sales/forecasting.mdx 把"三个桶互相独立"更正为"累进阶梯",并说明期间边界是算出来的、不是填出来的。

Testing

  • Unit tests pass (pnpm test)
  • Linting passes (pnpm lint)
  • Build succeeds (pnpm build)
  • New tests added

test/flow-scheduled.test.ts 新增 10 个 runtime 用例,跑真实的 AutomationEngine(不是 PENDING_FLOWS 豁免):

  • 只为活跃负责人开行;rep3(没有任何商机)和 rep4(只有 lost 单)不得到行
  • 期间字段日历对齐(period_start / period_end / period_label / snapshot_date),由 hook 派生
  • 累进分桶金额精确断言(best_case 断言 80,000 而非 50,000,专门钉住"累进"而不是"求和发生了")
  • 排除跨期间与 lost 单
  • 字符串金额被强制转数(amount: '25000'),防止模板拼接成 "020000"
  • 幂等:第二次扫描刷新同一行而不新增;也不会把金额累加成两倍
  • 覆盖过期数字(金额可以变小,这正是 create-once writer 做不到的)
  • 从不写 quota
  • 不碰属于其它期间的快照
  • 无人拥有活跃商机时干净空跑

反向验证(mutation check):把 best-case 过滤器从 { $in: ['best_case', 'commit'] } 改成 'best_case',测试如预期失败(- 80000 / + 50000),确认断言不是空转。

 Test Files  34 passed (34)
      Tests  662 passed | 1 skipped (663)
✓ Validation passed (946ms)   Logic: 24 Flows
✓ Build complete              → Skipping legacy runtime bundle (all 24 callables are body-only)
✓ source hygiene clean
tsc --noEmit                  (clean)
pnpm lint                     1 warning, 13 suggestions — 全部为既有项,无新增

Additional Notes

为什么没有用 script 节点做聚合。 这条路看起来最直接,但 script 的 config.script 是 no-op,真正执行需要 defineStack({ functions })。而顶层 functions 不参与 body 提取(见 @objectstack/cli 的 lower-callables),一旦声明,objectstack build 就会开始产出 objectstack-runtime.{hash}.mjs,并在 artifact 里写下 runtimeModule。而 scripts/publish-marketplace.mjs 只上传 dist/objectstack.json —— 发布到 marketplace 的 app 会引用一个根本没随行的模块,扫描在真实部署里直接报 "no function named … is registered"。本 PR 构建后 runtimeModule 仍为 (none),发布形态未变。

为什么 flow 里是"先建行、再读回、最后写数"。 期间窗口是数据,不是算术。flow 选出"窗口包含今天"的那一行(period_start <= today 且 period_end >= today,不需要任何日历运算),由 hook 在创建时给出日历对齐的窗口,再把边界读回来用于圈定商机。全流程只有一处定义"当前期间",且归对象所有。

check_missing 的两条出边都是 conditional 且互斥,共同汇入 reload_forecast。这里刻意不用"conditional + default"的组合:traverseNext 会把两类边都走一遍,那样整条链路会被执行两次、金额翻倍。


Generated by Claude Code

`crm_forecast` documented a nightly writer that did not exist: nothing in
`src/` ever created a snapshot row, so the forecast object, its attainment /
coverage formulas, the `forecast_metrics` dataset and the dashboard's
quota-attainment table all ran on three hand-seeded demo rows.

Add `forecast_snapshot`, a scheduled flow (nightly 03:00, runAs system) that
upserts one current-quarter row per active opportunity owner from
`crm_opportunity`: open pipeline, best case, commit and closed-won totals.
Re-runs refresh the same row instead of inserting another, and `quota` is
never written.

Two supporting changes:

* `forecast.hook.ts` now derives `period_start` alongside `period_end` and
  `period_label`. A flow template resolves `{TODAY()}` and day offsets but
  cannot express a calendar quarter boundary, so the object owns that
  derivation for every writer — the invariant #530 pinned for the seeds now
  also holds for runtime snapshots.
* `best_case_amount` / `commit_amount` descriptions now name the stored
  `forecast_category` boundary they actually measure, instead of probability
  thresholds no writer applied and that disagreed with the stage → category
  map.

`makeFlowHarness` gains an optional `hooks` option so a runtime test can
exercise flow + hook together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 2, 2026 11:34am

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 11:38
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 66fa27f Aug 2, 2026
10 checks passed
yinlianghui added a commit to yinlianghui/hotcrm that referenced this pull request Aug 10, 2026
…ur paragraph in zh and align best_case / closed_amount with the locale pack (objectstack-ai#836, objectstack-ai#845) (objectstack-ai#879)

The zh-Hans/zh-Hant `## Scheduled automation` sections carried two of the
English page's three paragraphs. The missing one (objectstack-ai#615) is the only place that
gives the criterion for splitting work between a flow and an object hook: the
Forecast Snapshot flow decides who gets a snapshot and what the totals are,
while the forecast object's hook decides which calendar period it belongs to,
because a cron schedule can say "every night" but not "the first day of this
quarter". Both zh pages now carry it, in each page's own vocabulary.

A sweep of the rest of the page found no other paragraph-count gap; every other
section already matches the English page one paragraph for paragraph.

`crm_opportunity.fields.forecast_category.options.best_case` ships as
"最佳情况" in `src/translations/zh-CN.ts`, but the zh docs spelled it three
ways. The two wrong ones are gone: "最佳可能" on the Opportunities pages (four
occurrences, each in the same sentence as an already-correct "承诺(Commit)")
and "最好情况" on the automation pages. The same automation row named the fourth
forecast total "赢单合计", a word the pack ships nowhere; `crm_forecast.closed_amount`
is labelled "已成交金额", which the Forecasting page already used.

Documentation only — nothing under `src/` changed, and the English pages, which
already match the pack, are untouched.

Fixes objectstack-ai#836
Fixes objectstack-ai#845


Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa

Co-authored-by: Claude <noreply@anthropic.com>
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 metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forecast has no writer — implement the scheduled snapshot flow the object docs promise

2 participants