Repository navigation
feat(forecast): add the forecast_snapshot nightly writer (#590) - #615
Merged
Merged
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-zhuang
marked this pull request as ready for review
August 2, 2026 11:38
This was referenced Aug 5, 2026
Closed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
pnpm test)pnpm lint)pnpm build)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,专门钉住"累进"而不是"求和发生了")amount: '25000'),防止模板拼接成"020000"quota反向验证(mutation check):把 best-case 过滤器从
{ $in: ['best_case', 'commit'] }改成'best_case',测试如预期失败(- 80000 / + 50000),确认断言不是空转。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