Repository navigation
quota_attainment_by_rep sums forecast rows across every period, so month + quarter snapshots double-count #614
Description
Activity
- addedbugSomething isn't workingSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removed
on Aug 2, 2026 认领:PM 循环第 4 轮
会话:session_019SS7C5SXpniKeCApxgARyf
分支:claude/issue-614-quota-attainment-period
Worktree:hotcrm-issue-614优先级从 P1 上调为 P0:#590 落地后
crm_forecast变成活表,这张表现在会把同一个人的季度配额、月度配额、上季度配额加在一起当成"配额"——种子数据下就是 1.5M 的真实季度配额显示为 3.4M,达成率同比例失真。客户预览里销售仪表盘上一个明显算错的数字,比缺一个功能更伤信任,所以它该在预览前修掉。issue 给了三条路线,请先核实哪条真的可行再选:方案 1 需要一个分析路径真的能解析的日期宏——
src/views/forecast.view.ts里已有注释说明列表路径一个都解析不了,分析路径必须单独验证,不要从列表路径的结论外推。方案 3(给forecast_metrics加 defaultFilter)会影响所有消费方,若选它请说明还有谁在读这个数据集。验收按 issue 原话衡量:种子数据加一次 sweep 后,表里某个销售的 Quota 列等于他的季度配额,而不是所有快照之和。请把改前改后的实际数字都放进报告。
同批:#622(
demo-bootstrap.flow.ts/contract.hook.ts/ 种子)、#598(线索相关)。共用桶文件提醒:content/docs/guides/index*.mdx与meta*.json,只有真新增文档页才碰。
Generated by Claude Code
PM 复核:通过,已入队合并 — #628
核验依据(以 GitHub 为准):9 项检查全绿;9 个文件全部在范围内(widget、数据集契约注释、四语言翻译、文档一处更正、新测试、changeset);未碰
src/data/index.ts(本轮归 #622)与共用的content/docs/guides/索引。实际数字比我在 issue 里估的更糟:不是 3.4M,而是 7,940,000——因为 #591 把预测种子从 3 行加到了 8 行,达成率显示 90%,真实值 55%。这也说明这类"跨维度求和"的缺陷会随数据增长而恶化,越晚修越难看。
方案选择做了实测而非推理,三条路线逐条给了结论:
- 方案 2(current_quarter 布尔维度)不可行——该标记必须随时间变化,只能做 formula,而 formula 是引擎查完后在 JS 里算的,数据引擎看不到这一列。本仓库已经踩过同一个坑(
forecast.view.ts里attainment_pct desc排序失效)。这条路会产出一个"过滤不掉任何东西"的过滤器。 - 方案 3(数据集 defaultFilter)可行但不该做——已全量 grep 确认
forecast_metrics当前只有这一个消费方(10 个 report 全绑在别的数据集上),所以影响面小;不选的理由是长期形状:period/period_label这两个维度存在的意义就是跨周期趋势,数据集自带"只看最新季度"会让将来任何配额趋势图静默只返回一行。我认可这个取舍标准——缺 widget 过滤器的失败模式是一个会红的测试,藏在数据集里的过滤器的失败模式是悄悄少了数据。 - 方案 1 实测可行。我在认领里特别要求"分析路径必须单独验证、不要从列表路径外推",这一条照做了:
queryDataset在任何 strategy 执行前先把 widget 的runtimeFilter过一遍resolveFilterTokens,编译出的 SQL 是WHERE (period = $1 AND period_start = $2),宏成为绑定参数;而列表路径确实一个宏都解析不了,所以this_quarter_forecasts视图保持只用操作符过滤,两条路径没有互相外推。
两个过滤条件缺一不可,这点给了对照数据:只按
period仍把历史每个季度相加(6,500,000);只按period_start又会把 Q3 2026 与 Jul 2026 合并(两者period_start同为2026-07-01,1,980,000);两者同时才唯一命中一行(1,500,000)。验收按 issue 原话跑出来了:真实服务 + 真实 SQLite,改前
quota_sum: 7940000 / attainment: 0.90,改后quota_sum: 1500000 / closed_sum: 900000 / attainment: 0.6——1,500,000 正是该销售 Q3 2026 的季度配额。守卫测试做了变异验证(摘掉 widget 的filter,3 个测试变红且报错文案直指问题)。已知代价已写进注释和 changeset 并被接受:季度切换到当晚 03:00 sweep 建出新行之间,表是空的。空表比在写着"本季度"的表头下显示上季度达成率更诚实。
派生发现 #627(预测文档称达成率"没有专属字段",而
crm_forecast.attainment_pct正是)已另开,未夹带。
Generated by Claude Code
- 方案 2(current_quarter 布尔维度)不可行——该标记必须随时间变化,只能做 formula,而 formula 是引擎查完后在 JS 里算的,数据引擎看不到这一列。本仓库已经踩过同一个坑(
- added 4 commits that reference this issue
on Aug 10, 2026 - addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removed
on Sep 9, 2026
Found while implementing #590. Pre-existing, filed unassigned rather than fixed in that PR.
The problem
The Sales dashboard's quota table binds the
forecast_metricsdataset with no period filter:quota_sumandclosed_sumare plainsummeasures overcrm_forecast, andattainmentisratio(closed_sum, quota_sum).crm_forecastholds one row per owner per period — and deliberately holds more than one period at a time: the seeds ship a current-quarter row, a current-month row and a previous-quarter row.So the table currently adds a quarter's quota to a month's quota to last quarter's quota for the same owner and calls the result "Quota". The seeded numbers make it concrete: 1,500,000 (this quarter) + 500,000 (this month) + 1,400,000 (last quarter) = a 3.4M "quota" for a rep whose real quarterly quota is 1.5M. The attainment percentage is wrong by the same construction.
The same applies to any widget or report that aggregates
forecast_metricswithout pinning a period —periodandperiod_labelare declared dimensions precisely because rows are not comparable across them.Why it surfaced now
#590 made
crm_forecasta live table instead of three demo rows. Today the sweep writes quarterly rows only, so it does not add a new kind of double count — but it does multiply the row count, and the moment a monthly cadence is added the error compounds per owner.Suggested direction
Pin the widget to a single period. Either:
filter: { period: 'quarter' }plus aperiod_startequality to the widget (needs a date-macro the widget filter path actually resolves — note the comment insrc/views/forecast.view.tsthat the list path resolves none, so check the analytics path separately before relying on one); orcurrent_quarterboolean/flag dimension the dataset can filter on; orforecast_metricsa defaultFilter so every consumer inherits "latest quarter" unless it opts out.Whatever the mechanism, the acceptance test is: with the seeded data plus one sweep run, the table's Quota column for a rep equals that rep's quarterly quota, not the sum of all their snapshots.