Skip to content

fix(data): give the demo accounts billing addresses so the territory rules match records (#638) - #641

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-638-seed-billing-addresses
Aug 2, 2026
Merged

fix(data): give the demo accounts billing addresses so the territory rules match records (#638)#641
os-zhuang merged 1 commit into
mainfrom
claude/issue-638-seed-billing-addresses

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #638

问题

#621 修好了"安装"那一半:两条区域规则现在能编译、能播种(seeded: 9, skipped: 0)。
"数据"那一半仍然是空的——9 个种子客户没有一个带 billing_address,于是两条规则在
9 条记录上正确求值,匹配 0 条。Setup 里能看到两个区域、两个 position,后面什么都没有。

改了什么:9 个客户的地址,跨三种结果分布

客户 国家 落入
Acme / Globex / Initech / Wayne / Vertex US north_america_territory
Stark Medical CA(多伦多) north_america_territory
Northwind Energy DE(慕尼黑) europe_territory
Lattice Education UK(伦敦) europe_territory
Apex Logistics SG(新加坡) 两个区域都不落

那条落在区域外的记录是承重的,不是凑数:如果 9 个客户全都落进某个区域,一条退化成
match-all 的规则和一条正常工作的规则在演示上完全一样,#621 那类缺陷就没有任何观测点。
按 issue 的建议分布,规则是可演示地划分这批客户:6 / 2 / 1。

country 写的是规则匹配的两字母代码(UK 而不是 ISO 的 GB)——投影只做 trim +
大写,不做翻译,所以 'United States' 会静默地掉出所有区域。四个换了国家的客户的电话
号码也跟着换了(慕尼黑的客户挂着丹佛的号码,是评估者在看到功能之前就会注意到的不一致)。

种子写入到底跑不跑钩子(#617 必须先答的那一问)

跑。 这不是推断,是实测。pnpm dev 起一个全新库,直接读 SQLite:

sqlite> SELECT name, billing_country FROM crm_account ORDER BY name;
Acme Corporation|US        Apex Logistics|SG       Globex Industries|US
Initech Solutions|US       Lattice Education|UK    Northwind Energy|DE
Stark Medical|CA           Vertex Analytics|US     Wayne Enterprises|US

billing_country 全程只有一个写者:account_protectionbeforeInsert 投影。种子里
一个字都没写它,而 9 行全都带上了值 —— 钩子确实在种子写入上跑了。

平台自己的文档也是这么说的(SeedLoaderService.SEED_OPTIONS,@objectstack/metadata-protocol):

  • isSystem —— 绕过 RBAC,并关闭 organization_id / owner_id 的自动注入(这正是种子行
    无主、需要 demo_bootstrap 认领的原因);
  • skipTriggers —— 抑制的是记录变更自动化(autolaunched flow),原文:
    "Lifecycle HOOKS (derived/default fields, validation) still run";
  • seedReplay —— 跳过 state_machine 的入口/迁移校验,好让中途状态的种子行(closed_won)能落库。

所以 src/data/_shared.ts 里的种子教条块已按实测重写:前提改掉,结论保留——"钩子拥有的
字段,种子里写的值必须等于钩子会算出的值"在两种前提下都成立,只是失效时机从"第一次用户编辑"
提前到了"播种当场"。#617 可以据此收口。

为什么在种子里补写 billing_country

因为钩子会跑,所以不需要;更重要的是不应该。billing_countryreadonly 的派生列,种子里
手抄一份就等于给一个钩子已经拥有的值造了第二个真值源——正是那段教条要防的漂移。教条块现在
把这一条显式写成第 3 条规则。测试从反方向钉住这个依赖:每个种子地址都必须带一个投影能变成
区域归属的国家码。

顺带做掉的结构性拆分(#635)

src/data/index.ts 距 100KB 上限只剩 1.5KB,9 个地址塞不进去;而在时间压力下最省字节的出路
是删注释,恰好是这个上限想保护的东西。按 #635 的首选方案拆:

src/data/_shared.ts       种子教条 + 跨族helper(celDaysAgo/LineSpec/linesTotal)
src/data/catalog.seed.ts  产品目录 + catalogPrice / lineItemRecords
src/data/sales.seed.ts    客户、联系人、线索、商机、商机行项
src/data/service.seed.ts  任务、工单、知识库
src/data/marketing.seed.ts 市场活动、活动成员
src/data/revenue.seed.ts  合同、报价(含行项)、预测
src/data/index.ts         只剩聚合导出 CrmSeedData(15 → 3.4KB)

拆分是数据等价的,而且是证明过的:在同一个进程里同时 import 本分支和 origin/main
两份种子,按 object + externalId 配对逐条 diff:

datasets before/after: 16 16
DIFF crm_account #0 Acme Corporation: +[billing_address] -[] ~[]
… (9 条,全部是本 PR 的地址;其中 4 条另有 ~[phone])
total record diffs: 9

除这 9 条外没有任何记录变化,也没有数据集丢失或新增。

测试

新增 test/territory-seed-coverage.test.ts(8 个用例)。它不复刻任何逻辑,而是用平台自己的
零件把整条链走一遍:

种子记录 → 真实的 account_protection handler → billing_country
        → compileCelToFilter(rule.condition)(plugin-sharing 播种时调用的同一个函数)
        → 每个区域实际覆盖哪些客户

断言:每个客户都有两字母国家码;种子不得书写 billing_country;钩子跑完每行都有投影;
NA / EU / 两者皆非 三个桶都非空;两个区域互斥;NA 桶不能退化成只有 US 一个国家
(否则 $in 列表其余部分等于没测)。

非空洞验证:把 SG 那条改成 US 后测试立刻转红,报的是有信息量的失败:

× partitions the seeded accounts across NA, EU and neither
AssertionError: every seeded account falls in a territory, so a match-all regression would
look identical to a working rule — NA=7 […] · EU=2 […] · neither=0 []

另有一组用例专治拆分自身唯一的新失败模式:某个 family 模块加了数据集却没接进 CrmSeedData
(编译通过、源码里有记录、就是永远不播种)。

验证

pnpm test        42 files / 948 passed | 1 skipped   (原 41 / 935)
pnpm typecheck   exit 0
pnpm validate    ✓ Validation passed (819ms)
pnpm lint        1 warning / 13 suggestions —— 与 main 基线逐字相同(已 stash 对照)
pnpm hygiene     ✓ no source file over 100KB · ✓ source hygiene clean
pnpm build       ✓ Build complete — dist/objectstack.json (1139.7 KB)

真机(pnpm dev,空库)播种后的实际划分:

north_america_territory (6): Acme, Globex, Initech, Stark Medical, Vertex, Wayne
europe_territory        (2): Lattice Education, Northwind Energy
neither                 (1): Apex Logistics

播种出来的规则行确实拿着这两个过滤器:

{"name":"north_america_territory","criteria_json":"{\"billing_country\":{\"$in\":[\"US\",\"CA\",\"MX\"]}}"}
{"name":"europe_territory","criteria_json":"{\"billing_country\":{\"$in\":[\"UK\",\"DE\",\"FR\",\"IT\",\"ES\"]}}"}

需要维护者知道的三件事

  1. 第三层仍然是空的,已另开 Nobody holds any position on a fresh install, so every position-based sharing rule and both approval flows resolve to an empty recipient set #640:sys_user_position 在全新安装上是 0 行,所以匹配到的
    客户依旧不给任何人授权(sys_record_share = 0)。按认领意见的边界,这不是 demo_bootstrap
    里的一两行:唯一的用户已经是全部记录的 owner + 管理员,把他塞进两个 team 授不出任何可观测的
    权限;要真的演示区域共享,需要非管理员的演示用户、每人持哪些 position 的决定,而同一个
    决定还牵动另外 7 个 position、4 个审批节点和 lead_assignmentsales_rep 池。Nobody holds any position on a fresh install, so every position-based sharing rule and both approval flows resolve to an empty recipient set #640 把实测
    数据和待定的三个问题都写清楚了,交由排期。
  2. content/docs/ 一个字没动:管理员文档里关于区域规则的描述本来就是准确的(它讲的是机制,
    不是演示数据),本 PR 不给它加演示数据细节,以免在三个语言版本里制造漂移。
  3. content/docs/releases/ 未触碰;本次的发布输入是 changeset。

🤖 Generated with Claude Code

https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf


Generated by Claude Code

…rules match records (#638)

#621 fixed the installation half of territory sharing: `north_america_territory`
and `europe_territory` now compile and seed against the flat
`crm_account.billing_country` column. The data half was still empty. Grepping
the repo, `billing_address` appeared in object definitions, views, translations,
the lead-conversion flow and the sharing rules — never in the seeds. So both
rules evaluated correctly over nine accounts and covered ZERO of them: Setup
listed two territories, two positions, and nothing behind either.

All nine accounts now carry a billing address, distributed across all THREE
outcomes the rules can produce rather than bulk-loaded into one territory:

  north_america_territory  ["US","CA","MX"]           6  (5 × US, Stark Medical CA)
  europe_territory         ["UK","DE","FR","IT","ES"] 2  (Northwind DE, Lattice UK)
  neither territory                                   1  (Apex Logistics SG)

The account outside both is load-bearing, not filler: with every account inside
a territory, a rule that degraded to match-all would look identical to a working
one. `country` carries the two-letter code the rules name (`UK`, not the ISO
`GB`) because the projection trims and upper-cases but never translates. The
four relocated accounts' phone numbers moved with them.

`billing_country` is deliberately NOT authored in the seeds. Measured on
17.0.0-rc.1 against a fresh install, hooks DO run over seed writes — all nine
rows come out of `pnpm dev` carrying the projection that only
`account_protection` writes:

  sqlite> SELECT name, billing_country FROM crm_account;
  Acme Corporation|US   Stark Medical|CA   Northwind Energy|DE
  Apex Logistics|SG     Lattice Education|UK   …

That settles #617: the seed doctrine's premise ("the loader uses skipTriggers,
hooks never run over seed rows") was wrong, and the platform says so itself —
`SeedLoaderService.SEED_OPTIONS` documents `skipTriggers` as suppressing
record-change AUTOMATION for seed writes while "Lifecycle HOOKS
(derived/default fields, validation) still run". The doctrine block is rewritten
around the corrected premise; its conclusion (a seeded value of a hook-owned
field must equal what the hook computes) holds either way and is kept. A
readonly, hook-derived column is now explicitly out of bounds for a seed: a
hand-copied `billing_country` would be a second source of truth for a value the
hook already owns.

`test/territory-seed-coverage.test.ts` refuses to let this go quiet again. It
walks the whole chain with the platform's own pieces — seed record → the real
`account_protection` handler → `compileCelToFilter` (the call plugin-sharing
makes) → which accounts each territory covers — and fails when any of the three
buckets empties out. Verified non-vacuous: moving the SG account into the US
turns it red.

Seed fixtures are split by object family (#635). `src/data/index.ts` was 1.5KB
under the 100KB source-hygiene cap, so nine addresses did not fit, and the
cheapest way under the cap would have been deleting the comments the cap exists
to protect. The file is now the aggregating `CrmSeedData` export over
`_shared.ts` (doctrine + cross-family helpers), `catalog.seed.ts`,
`sales.seed.ts`, `service.seed.ts`, `marketing.seed.ts` and `revenue.seed.ts`.
The split is provably data-identical: importing both revisions in one process
and diffing the aggregated datasets reports 16 datasets before and after with
the only record differences being the nine addresses and four phone numbers.
A second test fails if a family module's dataset is ever left out of
`CrmSeedData`, which is the one failure mode the split introduces.

Not fixed here: no user holds `na_sales_team` / `eu_sales_team` on a fresh
install (`sys_user_position` is empty, so `sys_record_share` stays empty), which
needs demo-user design rather than seed data. Filed separately.

Co-Authored-By: Claude Opus 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 4:11pm

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 16:13
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit adeadd2 Aug 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

No seeded account carries a billing_address, so both territory sharing rules match zero records on the demo dataset

2 participants