Skip to content

Commit 4145210

Browse files
feat(spec): currencyMode: 'record' + currencyField — a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1) (#27268)
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9oTskgBVZmtxXLAEh251Cb)_ Fixes #27184 ⛔ not released alone — R1: pairs with #27186, #27187, #27188 (one Version Packages release) Merge-after: #27218, #27233, #27239 (all three landed; `main` merged in at `73725c20`) Clause-②: yes (widening) Metadata-protocol: added Skills-docs: updated here Head: `b6f152bd` — CI fully green (65 check runs: 58 success, 7 skipped, 0 failed). Contract review PASS at `90114641` (comment 6487611079); its should-fixes are applied in `f3d286ce` and `b6f152bd` (see "Contract-review follow-ups" below). ## 维护者速读 - **改了什么:** 金额字段(及 `returnType: currency` 的公式)新增第三种币种模式 `record`:`currencyConfig.currencyField` 指向同对象上一个声明了 `valueDomain: 'iso_4217_currency'` 的文本字段,每条记录的币种就存在那里;金额本身仍是裸数字。前端 `resolveFieldCurrency` 对 `record` 模式字段返回 `undefined`(不回落租户默认币种),逐行币种由按行渲染读取。`record` 下 `currencyField` 不能为空,也不允许再写 `defaultCurrency`。 - **为什么改:** 平台此前没有任何键能表达「这条记录是什么币种」,这是多币种(ADR-0144 D2)的第一块,海外客户按客户币种开票依赖它。 - **风险与回滚:** 纯新增可选键(minor);不写 `record` 的应用零变化,`dynamic`/`fixed` 的解析输出逐字节不变(有测试钉住)。回滚即撤 PR。⛔ 不单独发版:与 #27186/#27187/#27188 同一次发布。 - **先看:** `packages/spec/src/data/object.zod.ts` 的 `refuseDishonestRecordCurrencyField`(三种拒收);`packages/spec/src/data/field.zod.ts` 的 `CurrencyConfigSchema` 与 `refuseRecordModeKeyMismatch`;`skills/objectstack-data/rules/field-types.md` 的币种段。 - **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-record-mode.test.ts`,预期 15 条全绿(含空指针、`record` 下的 `defaultCurrency`,以及指向不存在 / 非文本 / 无值域三种拒收及候选字段列表)。 - **需要你做的:** 三个前置(#27218、#27233、#27239)均已合并,`main` 已合入;契约复核在 `90114641` 已 PASS,其 should-fix 已在 `b6f152bd` 落实,等同一复核人的增量复核。本 PR 触及 `skills/**`(Tier H),落地需一次授权审批。 ## What changes - `CurrencyConfigSchema.currencyMode` gains `record` (ADR-0144 D2). `currencyField` (new, optional key) names the sibling field holding each record's ISO 4217 code. `currencyField` is never empty (`.min(1)`). A config-level refinement, `refuseRecordModeKeyMismatch`, makes it required under `record`, refuses it under `dynamic` / `fixed`, and refuses `defaultCurrency` under `record`, each at its own key with a prescription; it is chained after #27157's `refuseFixedCurrencyWithoutCode` on the same schema. `dynamic` keeps tolerating an inert `defaultCurrency` for the rows #27157 left behind; `record` is new, so it needs no such tolerance (ADR-0078). The amount stays a bare number (ADR-0104 D1, kept by ADR-0144 D2). - `ObjectSchema`'s `superRefine` gains `refuseDishonestRecordCurrencyField`, beside `refuseNonPictureImageField`: for every field whose config is `record`, the pointer must name another declared field whose type may carry a value domain (`VALUE_DOMAIN_FIELD_TYPES`, `text` today; the door reads the set rather than repeating the literal) and which declares `valueDomain: 'iso_4217_currency'`. Three refusals, each located at `fields.FIELD.currencyConfig.currencyField`, each carrying the prescription and the object's candidate code fields (or, when there are none, how to declare one). A `formula` with `returnType: 'currency'` meets both doors in the same shape, since it carries the same `currencyConfig`. - `field.form.ts`: the `record` option and a `currencyField` row in the currency composite; the `currencyMode` help text describes all three modes, `currencyConfig` says the value is a bare number "in every mode", and `defaultCurrency` says it is refused in record mode. The metadata-form translation bundles are regenerated (`check-i18n-bundles.mjs --write`), and the zh-CN / ja-JP / es-ES text for the new row and the three changed help texts is written by hand. - `@object-ui/i18n` `resolveFieldCurrency`: the parameter type accepts `record`, and a `record`-mode field resolves to `undefined` outright, with no fallback to the tenant default, `config.defaultCurrency` or a legacy `defaultCurrency`. The field has no currency of its own; a record whose code is blank shows no symbol rather than a currency it never named (aligned with #27340, the per-row display). - Generated: `content/docs/references/**` (gen:docs) and `packages/spec/authorable-surface/data.json` (gen:schema, `+ data/CurrencyConfig:currencyField`). The `dropped-refinements.baseline.json` sites this PR first added (the `data/CurrencyConfig` root plus `currencyConfig` under 12 schemas, header 252 / 900) are now identical to what #27218 landed, so the ledger is no longer in this diff. - `skills/objectstack-data/rules/field-types.md`: the currency block now covers all three modes, with a one-line record-mode example (`{ currencyMode: 'record', currencyField: 'currency_code' }`, the conventional name per ADR-0144 D8 rule 2). "A measure's own `currency` wins" is scoped to `fixed` / `dynamic` (#27338 refuses a measure `currency` over a record amount). The home-projection names `AMOUNT_home` / `AMOUNT_rate` are dropped until that projection exists. Paid for inside the same file: 322 → 320 lines, 3151 / 3158 tokens (ratchet green); no `SKILL.md` touched. - Changeset `.changeset/27184-currency-record-mode.md`: `@objectstack/spec` minor, `@object-ui/i18n` minor, `@objectstack/platform-objects` minor (it ships the regenerated metadata-form bundles). - ADR anchors: ADR-0144 added to `scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json` and `…object.zod.ts.json`, each with its D2 invariant. ## Tests - New `packages/spec/src/data/currency-config-record-mode.test.ts` (15 tests): `record` ⇔ `currencyField` at the config door (required / refused under `dynamic`, `fixed` and the defaulted mode); an empty `currencyField` refused in every mode; `defaultCurrency` refused under `record` (and still tolerated under `dynamic`); the three object-level refusals plus the self-pointer; the formula shape; `parse(parse(x))` equals `parse(x)` for configs and an object; `dynamic` and `fixed` outputs pinned byte-identical (`JSON.stringify`) to the post-#27157 output. Refusals assert `code`, `path` and the message's first sentence. - `ui/packages/i18n/src/__tests__/resolveFieldCurrency.test.ts`: one new case — a parsed `record` config resolves to `undefined` with a tenant default, with `config.defaultCurrency`, with a legacy `defaultCurrency`, and with no tenant default. - `packages/platform-objects/src/apps/translations/`: two hand-pinned catalog counts move for the new row — `field-panel-echo-decisions` UNWALKED_CHILDREN 10 → 11 (names `currencyConfig.currencyField`), and the `object-lifecycle-panel-echo-decisions` per-locale positive control 694 → 695 (main's #26674 took it 695 → 694). - Ablations, one per new refusal, through `scripts/ablation-replace.mjs` on the committed head (anchor 1 → 0 on disk, restored to the HEAD blob): `.min(1)` → `.min(0)` turns exactly "refuses an empty `currencyField`…" red (1 failed / 14 passed); disabling the `record` + `defaultCurrency` arm turns exactly "refuses `defaultCurrency` under `record`…" red (1 failed / 14 passed). - At `b6f152bd`: `@objectstack/spec` 835 files / 23779 tests pass, typecheck exit 0; `@objectstack/platform-objects` 97 files / 1369 tests pass. At `90114641`: `ui` `packages/i18n/` 89 files / 1374 tests pass, `@object-ui/i18n` type-check exit 0 (no ui file changed since). Earlier, at `bf7f2b74`, `ui` `packages/i18n/ plugin-detail/ plugin-gantt/ types/` 15806 tests pass. ## Acceptance notes - **`main` is merged in, not rebased.** Two merge commits: `94161588` (#27218) and `e527c3b6` (#27233, #27239, base `73725c20`), each followed by its own regeneration commit (`a9f5abfd`, `90114641`). The second merge conflicted only on the object-lifecycle positive-control count (combined to 695). The os-regen driver kept this branch's side of `authorable-surface/data.json`, which still listed the `CurrencyValue` keys #27233 retired; the baseline was taken from `main` and rebuilt, so the only line this PR adds there is `currencyField`. - **ADR-0144 citations checked against the landed text (`ad89eb22`).** Code, test, changeset and the two new anchors cite only D2, whose wording matches ("An amount field declares `currencyMode: 'record'`. Its `currencyField` names a text field on the same object with `valueDomain: 'iso_4217_currency'`"; the value stays a bare number, ADR-0104 D1 kept). The skills naming convention matches D8 rule 2. D-numbers are unchanged; nothing here cites Open item 12 or the Builds-on ADR-0112 sentence. `check-adr-anchors` is green. - **A producer that reads the new mode wrongly, outside this card's file surface:** `@objectstack/formula`'s `currencyOf` (`packages/formula/src/formula-return.ts`) maps every non-`fixed` config to `dynamic`, so `inferFormulaReturn` over a `record`-mode amount would stamp a currency formula with no `currencyConfig` (tenant currency) instead of `{ currencyMode: 'record', currencyField }`. Not reachable before this PR lands; carried as a declared rider on #27186. - `ObjectExtensionSchema` does not judge the pointer: an extension's fields merge into an object it does not hold, so its pointer may name an owner-declared field (stated in the helper's docblock). - `check:i18n`, `check:i18n-stale-fill` and `check:i18n-coverage` are green with the hand-translated three-mode texts. - The docs-drift bot comment lists hand-written pages naming `currencyConfig` / `currencyMode`; documenting the record mode there is left to #27189. ## Contract-review follow-ups (review 6487611079 at `90114641`, PASS) - ② `@objectstack/platform-objects` added to the changeset. - ④.1a `currencyField` `.min(1)`; ④.1b `defaultCurrency` refused under `record`; ④.1c / ④.2b ADR-0144 anchors on `field.zod.ts` and `object.zod.ts`. - ④.2a the object door reads `VALUE_DOMAIN_FIELD_TYPES`. - ④.3 / ③ zh-CN / ja-JP / es-ES `currencyMode` help text now describes three modes (plus the `currencyConfig` and `defaultCurrency` texts that the new rule changed). - ④.4a–c the skill: measure-currency sentence scoped, `AMOUNT_home` / `AMOUNT_rate` dropped, record-mode example added, within the ratchet. - Not this PR's and left as the review placed them: the share-link projection gap (escalated to the epic PM), the `os-zhuang` approval on `5232c8b1` (escalated to the maintainer), and the inherited `AddFieldOperation` ledger drop (#27218). ## Verification <details><summary>Dispatch gates — 123 derived, 123 run, all exit 0 at b6f152b (reconciled with --ran)</summary> Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `b6f152bd` (20 paths vs merge base `73725c20`); every family run with the closure built, each exit code recorded; reconciliation: "123 derived famil(ies) accounted for — 123 run, 0 NOT-MEASURED (a DERIVED zero — all 123 recorded an exit code and none of them is 3)". All 123 exit 0. `check:generated` (spec): all artifacts current. Earlier rounds, kept for the record: at `90114641`, 115 / 115 exit 0; at `5232c8b1`, 112 derived / 112 run, with `check-skills-token-ratchet` (fixed by deletion in the same file) and `check:i18n` (bundles regenerated and translated) red on the first pass and green after; at `a9f5abfd`, 112 / 112 exit 0. </details> <details><summary>CI at b6f152b — 65 check runs, 58 success, 7 skipped, 0 failed</summary> The same set as below, all success again: Lint & Repo Gates, TypeScript Type Check, Test Core (1–8/8), Test UI (1–16/16), Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Build Core / Docs / UI Deps, Check Changeset, PR-body guards, Governed Surface Queue Guard and the rest; the same 7 opt-in or schedule-gated jobs skipped. </details> <details><summary>CI at 9011464 (the reviewed head) — 65 check runs, 58 success, 7 skipped, 0 failed</summary> Success: Lint & Repo Gates (and its Lint · … jobs), TypeScript Type Check (and its Type Check · … jobs), Test Core (1–8/8), Test Core, Test UI (1–16/16), Test UI, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Build Core, Build Docs, Build UI Deps, Plan Test Core shards, Check Changeset, Check PR Size, Check Documentation Links, Spec property liveness, Governed Surface Queue Guard, Flag docs affected by code changes, PR-body guards, Auto Label, filter. Skipped (opt-in or schedule-gated): Dogfood Verify CLI, Dogfood Regression Gate matrix, Temporal Conformance, UI Published Artifacts, 18.x upgrade harness, Packed-tarball smoke, Schedule gate. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 273137d commit 4145210

20 files changed

Lines changed: 428 additions & 49 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@object-ui/i18n': minor
4+
'@objectstack/platform-objects': minor
5+
---
6+
7+
feat(spec): `currencyMode: 'record'` + `currencyField` — a record-mode amount points at a sibling ISO 4217 code field (ADR-0144 D2)
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
12+
A currency amount (or a `formula` with `returnType: 'currency'`) can now carry a currency per record. `currencyConfig.currencyMode` gains a third value, `record`, and `currencyConfig.currencyField` names the field on the same object that holds each record's ISO 4217 code. The amount itself stays a bare number (ADR-0104 D1).
13+
14+
```ts
15+
fields: {
16+
currency_code: Field.text({ label: 'Currency', valueDomain: 'iso_4217_currency' }),
17+
amount: Field.currency({ label: 'Amount', currencyConfig: { currencyMode: 'record', currencyField: 'currency_code' } }),
18+
}
19+
```
20+
21+
- `currencyField` is required under `record`, refused under `dynamic` and `fixed`, and never empty; `defaultCurrency` is refused under `record`.
22+
- The object refuses a `currencyField` that names no other declared field, a field that is not `text`, or a `text` field without `valueDomain: 'iso_4217_currency'`. Each refusal is located at `fields.FIELD.currencyConfig.currencyField` and lists the object's candidate code fields.
23+
- `dynamic` and `fixed` configs parse exactly as before; an app that never writes `record` sees no change.
24+
25+
⛔ Not released alone: this ships in the same release as the record-mode aggregate refusals and the per-row display.

‎content/docs/references/data/field.mdx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ const result = CurrencyConfigSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **currencyMode** | `Enum<'dynamic' \| 'fixed'>` | optional (default: `"dynamic"`) | Currency mode. `fixed`: the field has one currency, `defaultCurrency`. `dynamic` (the default): the field has no currency of its own — amounts display in the tenant default currency (the `localization.currency` setting; a plain number when none is set) and `defaultCurrency` is not read. Neither mode is a per-record choice: the value is a bare number either way. |
32-
| **defaultCurrency** | `string` | optional | The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency, required under `fixed`. Not read under `dynamic` (the default), where amounts display in the tenant default currency. |
31+
| **currencyMode** | `Enum<'dynamic' \| 'fixed' \| 'record'>` | optional (default: `"dynamic"`) | Currency mode. `fixed`: the field has one currency, `defaultCurrency`. `dynamic` (the default): the field has no currency of its own — amounts display in the tenant default currency (the `localization.currency` setting; a plain number when none is set) and `defaultCurrency` is not read. `record`: each record has its own currency, the ISO 4217 code stored in the sibling field `currencyField` names; `defaultCurrency` is refused. The value is a bare number in every mode (ADR-0104 D1). |
32+
| **defaultCurrency** | `string` | optional | The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency, required under `fixed`. Not read under `dynamic` (the default), where amounts display in the tenant default currency, and refused under `record`, where each record names its own. |
33+
| **currencyField** | `string` | optional | `record` mode only, and required there: the name of the field on the same object that holds each record's currency — a `text` field declaring `valueDomain: 'iso_4217_currency'` (conventionally `currency_code`). Refused under `dynamic` and `fixed`, and never empty. |
3334

3435

3536
---
@@ -89,7 +90,7 @@ const result = CurrencyConfigSchema.parse(data);
8990
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
9091
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
9192
| **step** | `number` | optional | Step increment for slider (default: 1) |
92-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
93+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed' \| 'record'>; defaultCurrency?: string; currencyField?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
9394
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
9495
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
9596
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
@@ -236,8 +237,9 @@ const result = CurrencyConfigSchema.parse(data);
236237

237238
| Property | Type | Required | Description |
238239
| :--- | :--- | :--- | :--- |
239-
| **currencyMode** | `Enum<'dynamic' \| 'fixed'>` | optional (default: `"dynamic"`) | Currency mode. `fixed`: the field has one currency, `defaultCurrency`. `dynamic` (the default): the field has no currency of its own — amounts display in the tenant default currency (the `localization.currency` setting; a plain number when none is set) and `defaultCurrency` is not read. Neither mode is a per-record choice: the value is a bare number either way. |
240-
| **defaultCurrency** | `string` | optional | The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency, required under `fixed`. Not read under `dynamic` (the default), where amounts display in the tenant default currency. |
240+
| **currencyMode** | `Enum<'dynamic' \| 'fixed' \| 'record'>` | optional (default: `"dynamic"`) | Currency mode. `fixed`: the field has one currency, `defaultCurrency`. `dynamic` (the default): the field has no currency of its own — amounts display in the tenant default currency (the `localization.currency` setting; a plain number when none is set) and `defaultCurrency` is not read. `record`: each record has its own currency, the ISO 4217 code stored in the sibling field `currencyField` names; `defaultCurrency` is refused. The value is a bare number in every mode (ADR-0104 D1). |
241+
| **defaultCurrency** | `string` | optional | The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency, required under `fixed`. Not read under `dynamic` (the default), where amounts display in the tenant default currency, and refused under `record`, where each record names its own. |
242+
| **currencyField** | `string` | optional | `record` mode only, and required there: the name of the field on the same object that holds each record's currency — a `text` field declaring `valueDomain: 'iso_4217_currency'` (conventionally `currency_code`). Refused under `dynamic` and `fixed`, and never empty. |
241243

242244
### Nested Shape: `Field.conditionalFormatting[number]`
243245

‎content/docs/references/data/object.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ const result = ApiMethod.parse(data);
268268
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
269269
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
270270
| **step** | `number` | optional | Step increment for slider (default: 1) |
271-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
271+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed' \| 'record'>; defaultCurrency?: string; currencyField?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
272272
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
273273
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
274274
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
@@ -631,7 +631,7 @@ const result = ApiMethod.parse(data);
631631
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
632632
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
633633
| **step** | `number` | optional | Step increment for slider (default: 1) |
634-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
634+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed' \| 'record'>; defaultCurrency?: string; currencyField?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
635635
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
636636
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
637637
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |

‎content/docs/references/system/migration.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Add a new field to an existing object
102102
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
103103
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
104104
| **step** | `number` | optional | Step increment for slider (default: 1) |
105-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
105+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed' \| 'record'>; defaultCurrency?: string; currencyField?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
106106
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
107107
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
108108
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
@@ -533,7 +533,7 @@ Add a new field to an existing object
533533
| **summaryOperations** | `{ object: string; field: string; function: Enum<'count' \| 'sum' \| 'min' \| 'max' \| 'avg'>; relationshipField?: string; … }` | optional | Roll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted. |
534534
| **language** | `string` | optional | Programming language for syntax highlighting (e.g., javascript, python, sql) |
535535
| **step** | `number` | optional | Step increment for slider (default: 1) |
536-
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
536+
| **currencyConfig** | `{ currencyMode?: Enum<'dynamic' \| 'fixed' \| 'record'>; defaultCurrency?: string; currencyField?: string }` | optional | Which currency an amount is in: for a currency field, and for a formula field whose returnType is currency. Absent: dynamic mode. |
537537
| **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) |
538538
| **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
539539
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,15 +618,19 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
618618
},
619619
currencyConfig: {
620620
label: "Currency Config",
621-
helpText: "Which currency this field is in — a currency field, or a formula whose return type is Currency. Unset: dynamic mode. The value is a bare number in either mode."
621+
helpText: "Which currency this field is in — a currency field, or a formula whose return type is Currency. Unset: dynamic mode. The value is a bare number in every mode."
622622
},
623623
"currencyConfig.currencyMode": {
624624
label: "Currency Mode",
625-
helpText: "dynamic (the default): the field has no currency of its own, and amounts display in the tenant default currency (the localization.currency setting). fixed: the field has one currency, defaultCurrency."
625+
helpText: "dynamic (the default): the field has no currency of its own, and amounts display in the tenant default currency (the localization.currency setting). fixed: the field has one currency, defaultCurrency. record: each record has its own currency, stored in the field currencyField names."
626+
},
627+
"currencyConfig.currencyField": {
628+
label: "Currency Field",
629+
helpText: "Record mode only, and required there: the field on this object holding each record's ISO 4217 code — a text field with valueDomain iso_4217_currency, conventionally currency_code."
626630
},
627631
"currencyConfig.defaultCurrency": {
628632
label: "Default Currency",
629-
helpText: "The one currency of a fixed-mode field, as a three-character ISO 4217 code (e.g. USD, EUR). Required in fixed mode; nothing fills one in. Not read in dynamic mode."
633+
helpText: "The one currency of a fixed-mode field, as a three-character ISO 4217 code (e.g. USD, EUR). Required in fixed mode; nothing fills one in. Not read in dynamic mode; refused in record mode."
630634
},
631635
step: {
632636
label: "Step",

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,15 +618,19 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
618618
},
619619
currencyConfig: {
620620
label: "Configuración de moneda",
621-
helpText: "En qué moneda está este campo: un campo de moneda, o una fórmula cuyo tipo de retorno es Moneda. Sin definir: modo dynamic. En ambos modos el valor es un número sin más."
621+
helpText: "En qué moneda está este campo: un campo de moneda, o una fórmula cuyo tipo de retorno es Moneda. Sin definir: modo dynamic. En todos los modos el valor es un número sin más."
622622
},
623623
"currencyConfig.currencyMode": {
624624
label: "Modo de moneda",
625-
helpText: "dynamic (predeterminado): el campo no tiene moneda propia y los importes se muestran en la moneda predeterminada del tenant (el ajuste localization.currency). fixed: el campo tiene una única moneda, defaultCurrency."
625+
helpText: "dynamic (predeterminado): el campo no tiene moneda propia y los importes se muestran en la moneda predeterminada del tenant (el ajuste localization.currency). fixed: el campo tiene una única moneda, defaultCurrency. record: cada registro tiene su propia moneda, guardada en el campo que indica currencyField."
626+
},
627+
"currencyConfig.currencyField": {
628+
label: "Campo de moneda",
629+
helpText: "Solo en modo record, y obligatorio en él: el campo de este objeto que guarda el código ISO 4217 de cada registro — un campo de texto con valueDomain iso_4217_currency, por convención currency_code."
626630
},
627631
"currencyConfig.defaultCurrency": {
628632
label: "Moneda predeterminada",
629-
helpText: "La única moneda de un campo en modo fixed, como código ISO 4217 de tres caracteres (p. ej., USD, EUR). Obligatorio en modo fixed; no se rellena automáticamente. No se lee en modo dynamic."
633+
helpText: "La única moneda de un campo en modo fixed, como código ISO 4217 de tres caracteres (p. ej., USD, EUR). Obligatorio en modo fixed; no se rellena automáticamente. No se lee en modo dynamic; se rechaza en modo record."
630634
},
631635
step: {
632636
label: "Incremento",

‎packages/platform-objects/src/apps/translations/field-panel-echo-decisions.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,9 +708,11 @@ describe('#19403 round 4 — the blind spot, FOURTH shape: a type-level coverage
708708
// locales, so the echo rule above reads them translated, not undecided.
709709
// 10 since #23735: the `referenceDeletedWhen` composite (`field`, `value`),
710710
// whose four string leaves that card authored in all three locales.
711-
expect(UNWALKED_CHILDREN.length).toBe(10);
711+
// 11 since #27184: `currencyConfig.currencyField` (the record mode's code
712+
// field), its two string leaves authored in all three locales.
713+
expect(UNWALKED_CHILDREN.length).toBe(11);
712714
for (const child of [
713-
'currencyConfig.currencyMode', 'currencyConfig.defaultCurrency', 'storage.notNull',
715+
'currencyConfig.currencyMode', 'currencyConfig.defaultCurrency', 'currencyConfig.currencyField', 'storage.notNull',
714716
'referenceDeletedWhen.field', 'referenceDeletedWhen.value',
715717
]) {
716718
expect(UNWALKED_CHILDREN).toContain(child);

0 commit comments

Comments
 (0)