Repository navigation
Commit 194e66c
feat(platform-objects): sys_exchange_rate — an organization's dated exchange rates, admin-written and audited, with a Setup list under Localization (#27246)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9oTskgBVZmtxXLAEh251Cb)_
Fixes #27185
⛔ not released alone — pairs with #27192 (R2); merges in the R2 window
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:** 新系统对象 `sys_exchange_rate`(币种 ISO 4217、汇率精确小数(小数点后 12 位、总共 24
位,裁决 6469382727)、生效日期、来源 manual/import/app),同组织内 (币种, 生效日期) 唯一;Setup →
Configuration 在 Localization 下面加「Exchange
Rates」列表页,页面上写明一次汇率方向;文档一行讲财务怎么录一个月的汇率。
- **为什么改:** 多币种 C′ 形状里写入时折算的唯一数据源(裁决 6465431966,参数 2/3 按默认;参数 4 由裁决
6469382727 定为 scale 12 / precision 24)。
- **风险与回滚:** 只新增一张表和一个菜单项,不改任何已有对象(另有三处 CI 强制的登记:spec 平台对象名单、CLI
升级仪式的组织归属清单、CLI 的 Setup 菜单条数断言);它唯一的读者是 #27192,⛔ 不单独发版,和 R2 同一次 Version
Packages 合并。回滚 = revert 本 PR(新表无数据依赖)。
- **先看:**
`packages/platform-objects/src/system/sys-exchange-rate.object.ts`(唯一键的
`'organization'` 拼法、`rate` 的 `scale`/`min` 与方向句);`src/plugin.ts`(注册进第一个
manifest,走项目库);`src/apps/setup-nav.contributions.ts`(菜单位置)。
- **两分钟验证:** `pnpm --filter @objectstack/platform-objects exec vitest
run src/system/sys-exchange-rate.object.test.ts`,预期 15 个测试全绿。
- **需要你做的:** 无,席位落地(R2 窗口里与 #27192 一起合)。
## What changes
- **`sys_exchange_rate`**
(`packages/platform-objects/src/system/sys-exchange-rate.object.ts`),
`managedBy: 'system-data'`:
- `currency` — text, `valueDomain: 'iso_4217_currency'`, required.
- `rate` — `number` (the platform's exact-decimal column, never
`float`), `scale: 12`, `precision: 24`, `min` = 10⁻¹² (strictly
positive), required (ruling 6469382727).
- `effective_date` — date, required. `source` — select `manual` /
`import` / `app`, default `manual`.
- Unique `(currency, effective_date)` spelled `unique: 'organization'`
(ADR-0120 D1), so two organizations each hold their own EUR rate for a
day.
- No tenancy opt-out: `organization_id` is injected and the Layer 0 wall
scopes it.
- `enable.trackHistory: true` plus per-field `trackHistory` on the four
ruled columns; the object is not on plugin-audit's skip list, so every
write lands in `sys_audit_log`.
- Who writes: `organization_admin`'s `'*'` grant reaches it (not
`private`, no `requiredPermissions`); `member_default` names no business
object, so members neither read nor write it (ruling parameter 2).
- **Rate direction stated once**: `EXCHANGE_RATE_DIRECTION` (exported)
is the `rate` field's description and the `all_rates` list view's
description, which the console renders on the list page.
`EXCHANGE_RATE_SCALE` (12) and `EXCHANGE_RATE_PRECISION` (24) exported
beside it.
- **Registration**: `PlatformObjectsPlugin` registers it in its first
manifest (project database, no routing triple), beside `sys_migration` /
`sys_migration_journal` / `sys_secret`: its reader is the ObjectQL
engine's write path, present in every composition.
- **Registry** (`@objectstack/spec`):
`PLATFORM_OBJECTS_BY_PACKAGE['platform-objects']` lists
`sys_exchange_rate`, so lint, the engine and the CLI treat it as a real
platform object; the spec ratchet `platform-object-names.test.ts`
requires it.
- **Upgrade ceremony** (`@objectstack/cli`): the ADR-0131 D10
organization-ownership inventory gives `sys_exchange_rate` fate 3
(attribution, no anchors: the Default Organization under `single`,
otherwise reported), like the other organization-authored tables added
since the first census; without the row `os migrate
organization-ownership` refuses every plan as carrying an uninventoried
platform table.
- **Setup**: `nav_exchange_rates` in `group_configuration` directly
under `nav_settings_localization`, opening `all_rates`.
- **Translations**: object, fields, options, view, empty state and the
nav label in `en` / `zh-CN` / `ja-JP` / `es-ES` (the three translated
locales hand-written; bundles regenerated with `check-i18n-bundles.mjs
--write`, extract config gains the object).
- **Docs**: `content/docs/ui/apps.mdx` (where `main` moved the Setup
anchors when it retired `ui/setup-app.mdx`) — the `group_configuration`
row and one paragraph saying how finance records a month's rates.
- **Changeset**: `.changeset/27185-exchange-rate-table.md`,
`@objectstack/platform-objects`, `@objectstack/spec` and
`@objectstack/cli` patch.
## Why
Multi-currency shape C′ (ruling 6465431966 on #27119; epic #8345, M1-1):
a record-mode amount is converted to the home currency at write from the
rate in force on its document date. This is that rate table. Parameters
2 (admin-only) and 3 (1 home = `rate` foreign, so home = amount ÷
`rate`) are applied as ruled; parameter 4 as ruled in 6469382727 (scale
12, precision 24).
## Tests
- New `src/system/sys-exchange-rate.object.test.ts` (15 cases) pins the
declaration each enforcing mechanism reads, on the built value: one
unique key on `(currency, effective_date)` equal to `'organization'`;
`resolveInjectedSystemColumns` plans `organization_id`; not `private`
and no `requiredPermissions` (the org-admin wildcard reaches it);
`resolveCrudAffordances` gives New/Edit/Delete and no import;
`trackHistory` on object and columns; `iso_4217_currency` with the write
predicate's member / non-member / lowercase answers; `rate` is `number`
with `scale` 12, `precision` 24 and `min` 10⁻¹²; the direction sentence
on field and view; the nav entry sits right after Localization and names
a declared view.
- Updated: `plugin.test.ts` (registration roster),
`platform-objects.test.ts` (nav target classified as owned and
listable), `bundle-ownership.test.ts` (bundle roster);
`packages/cli/test/connect-agent-both-halves-wire.pin.test.ts` (the
Setup tree the platform admin is served: 31 → 32 entries, 22 → 23
contributions). The spec ratchet `platform-object-names.test.ts` and the
CLI `organization-ownership-inventory.test.ts` pass unchanged with the
two registry rows.
## Acceptance notes
- **Behavioural layer not exercised here.** The card's four facts are
enforced by mechanisms pinned in their own packages (driver-sql's
tenant-scoped declared-unique suite; Layer 0; plugin-security
`default-permission-sets.ts`; plugin-audit `AUDIT_EXCLUDED_OBJECTS`).
`platform-objects` cannot import plugin-security or plugin-audit (they
depend on it), and the claimed file surface stops at this package, so no
live admin-201 / member-403 / duplicate-409 / cross-org-201 run is
recorded: NOT MEASURED, carrier #27197 (M3-3 dogfood seeds rates and
drives the browser).
- **CSV import stays off** (`system-data`'s default) until #27190
verifies `sys_import_job` serves this object; turning it on is
`userActions: { import: true }` there.
- **Parameter 4 is ruled (6469382727 on #27185):** scale 12 (twelve
digits after the decimal point), total precision 24, decimal arithmetic,
never float. The write seam refuses a value beyond either bound rather
than rounding it. The column is the platform's `DECIMAL(65,30)`
(`NUMERIC_COLUMN_REPRESENTATION`), which holds both.
- **Direction as arithmetic.** With parameter 3 as ruled, a home amount
is the record amount **divided by** `rate` (ADR-0144). Card #27192's
body writes the projection as amount × rate; that card should follow the
ruling.
- **SQLite stores `number` as REAL** (`table.decimal` compiles to
`float` on SQLite), and every dialect reads a `number` back as a JS
double (about 15–17 significant digits). A rate using all 24 digits is
beyond that and is not round-tripped exactly; #27192 already owns
pinning SQLite rounding.
- **Paths outside the claimed file surface**, each forced by a CI gate
on the new object:
- `scripts/platform-object-tenancy-census.json`, regenerated with `node
scripts/platform-object-tenancy-census.mjs --write` because the new
tenant-scoped object joins the census population (87 → 88 registered, 44
→ 45 in reach). Derived artefact, no hand edit.
- `packages/spec/src/system/constants/platform-object-names.ts` (one
name in the registry list; the spec ratchet reds without it).
- `packages/cli/src/utils/organization-ownership-inventory.ts` (one
inventory row; the CLI inventory test and about 15
`organization-ownership` integration tests red without it).
- `packages/cli/test/connect-agent-both-halves-wire.pin.test.ts` (the
Setup nav count).
- Not here by design: a rate row whose currency equals the home currency
(no refusal yet — the projection treats same-currency as rate 1 without
a lookup, #27192), and the home-currency lock (#27198).
- The ADR draft for #27183 (PR #27239, ADR-0144, not yet approved) was
read: the direction sentence follows it; the scale follows ruling
6469382727.
- **Owner package (ADR-0144 Open item 11, against ADR-0029 D1/D3/D6):**
`platform-objects`, because the table's one reader is the ObjectQL
engine's write path, present in every composition that can write a
record; it registers with the engine-facing infrastructure
(`sys_migration`, `sys_secret`) and has no on/off capability boundary of
its own (ADR-0029 D2). ADR-0144's M1-1 row names the same package.
## Verification
<details><summary>Final head 7ad4554 (after merging main, fresh
install and full build)</summary>
```
pnpm --filter @objectstack/platform-objects test Test Files 98 passed · Tests 1381 passed
pnpm --filter @objectstack/spec exec vitest run src/system/constants/platform-object-names.test.ts 12 passed
pnpm --filter @objectstack/spec check:generated all 14 generated artifacts up to date
pnpm --filter @objectstack/cli exec vitest run <inventory test, Setup-nav pin, 8 organization-ownership integration suites> 10 files · 164 passed
pnpm --filter @objectstack/{cli,platform-objects} typecheck OK
48 scoped re-checks after the merge (docs, i18n, census, changeset, citation gates and the above) :: all exit 0
Before the merge, on 3e26252: pnpm --filter @objectstack/cli test 521 files · 6493 passed
dispatch-gates --commands: 115 families; the 3 added by the CLI paths (i18n-coverage, i18n-walk-parity,
cli-test-child-env) :: exit 0; the list did not change across the merge.
```
</details>
<details><summary>Earlier heads: package test + typecheck (lock
VERDICT)</summary>
```
head 86e39fa (final):
pnpm --filter @objectstack/platform-objects test
Test Files 97 passed (97)
Tests 1377 passed (1377)
os-verify-lock: VERDICT command-exit 0
pnpm --filter @objectstack/platform-objects typecheck
check:test-typecheck: OK; os-verify-lock: VERDICT command-exit 0
node scripts/check-i18n-bundles.mjs :: exit 0 (platform-objects in sync, 11 bundles)
```
</details>
<details><summary>Derived gates (dispatch-gates --commands, --ran
reconciliation)</summary>
```
dispatch-gates --commands: 98 families (90 derived first, +8 after the census artefact joined the diff).
90 run on head faf1b6e (before the scale/census commit): 86 exit 0 at first run;
4 refused/red, then resolved:
- check-plugin-teardown-shape --self-test :: exit 3 (shallow clone) -> fetched pinned commit -> exit 0
- spec check:skill-examples :: exit 3 (client-react unbuilt) -> built -> exit 0
- check:dual-build-cjs-loads :: exit 3 (unbuilt packages) -> exit 0 after the build
- check:platform-object-tenancy-census :: exit 1 (census drift, new object) -> --write -> exit 0
On head 86e39fa: the 8 new families :: exit 0; re-run on the final tree, all exit 0:
check:platform-object-tenancy-census, check:i18n (bundles), check:keyed-text-bounds,
check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:org-identifier,
check:system-context-census, check:issue-citations, check:doc-authoring, check:i18n-stale-fill
dispatch-gates --ran: 98 derived famil(ies) accounted for - 98 run, 0 NOT-MEASURED
(a DERIVED zero - all 98 recorded an exit code and none of them is 3).
Not run locally (CI's): pnpm lint, test:repo, the full farm.
```
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(The spec change is one name in a constant list.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched only to list the new object in
`PLATFORM_OBJECTS_BY_PACKAGE`, with its `patch` bump in the changeset.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e00374a commit 194e66c
26 files changed
Lines changed: 588 additions & 15 deletions
File tree
- .changeset
- content/docs/ui
- packages
- cli
- src/utils
- test
- platform-objects
- scripts
- src
- apps
- translations
- system
- spec/src/system/constants
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
813 | 813 | | |
814 | 814 | | |
815 | 815 | | |
816 | | - | |
| 816 | + | |
817 | 817 | | |
818 | 818 | | |
819 | 819 | | |
820 | 820 | | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
821 | 827 | | |
822 | 828 | | |
823 | 829 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
781 | 781 | | |
782 | 782 | | |
783 | 783 | | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
784 | 787 | | |
785 | 788 | | |
786 | 789 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | | - | |
| 257 | + | |
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
264 | | - | |
| 264 | + | |
265 | 265 | | |
266 | 266 | | |
267 | 267 | | |
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
287 | | - | |
288 | | - | |
| 287 | + | |
| 288 | + | |
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
| |||
317 | 317 | | |
318 | 318 | | |
319 | 319 | | |
| 320 | + | |
320 | 321 | | |
321 | 322 | | |
322 | 323 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
129 | 135 | | |
130 | 136 | | |
131 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3530 | 3530 | | |
3531 | 3531 | | |
3532 | 3532 | | |
| 3533 | + | |
| 3534 | + | |
| 3535 | + | |
| 3536 | + | |
| 3537 | + | |
| 3538 | + | |
| 3539 | + | |
| 3540 | + | |
| 3541 | + | |
| 3542 | + | |
| 3543 | + | |
| 3544 | + | |
| 3545 | + | |
| 3546 | + | |
| 3547 | + | |
| 3548 | + | |
| 3549 | + | |
| 3550 | + | |
| 3551 | + | |
| 3552 | + | |
| 3553 | + | |
| 3554 | + | |
| 3555 | + | |
| 3556 | + | |
| 3557 | + | |
| 3558 | + | |
| 3559 | + | |
| 3560 | + | |
| 3561 | + | |
| 3562 | + | |
| 3563 | + | |
| 3564 | + | |
| 3565 | + | |
| 3566 | + | |
| 3567 | + | |
| 3568 | + | |
| 3569 | + | |
| 3570 | + | |
| 3571 | + | |
| 3572 | + | |
| 3573 | + | |
3533 | 3574 | | |
3534 | 3575 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
| 127 | + | |
127 | 128 | | |
128 | 129 | | |
129 | 130 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3530 | 3530 | | |
3531 | 3531 | | |
3532 | 3532 | | |
| 3533 | + | |
| 3534 | + | |
| 3535 | + | |
| 3536 | + | |
| 3537 | + | |
| 3538 | + | |
| 3539 | + | |
| 3540 | + | |
| 3541 | + | |
| 3542 | + | |
| 3543 | + | |
| 3544 | + | |
| 3545 | + | |
| 3546 | + | |
| 3547 | + | |
| 3548 | + | |
| 3549 | + | |
| 3550 | + | |
| 3551 | + | |
| 3552 | + | |
| 3553 | + | |
| 3554 | + | |
| 3555 | + | |
| 3556 | + | |
| 3557 | + | |
| 3558 | + | |
| 3559 | + | |
| 3560 | + | |
| 3561 | + | |
| 3562 | + | |
| 3563 | + | |
| 3564 | + | |
| 3565 | + | |
| 3566 | + | |
| 3567 | + | |
| 3568 | + | |
| 3569 | + | |
| 3570 | + | |
| 3571 | + | |
| 3572 | + | |
| 3573 | + | |
3533 | 3574 | | |
3534 | 3575 | | |
0 commit comments