Skip to content

Commit 9e7824a

Browse files
docs(spec): currencyMode says what each mode does; the currency-chain text states the fixed condition (#20179)
Part of #20126 Clause-②: no This PR covers the non-governed sites of the currency-mode class-closure card. It does not close the card. The remainder is listed at the end: the Tier H skills site and the enumerating pin. ## What was wrong `CurrencyConfigSchema` declares two modes. With `currencyMode: 'fixed'`, the field has one currency, `defaultCurrency`. With `dynamic`, the default, the field has no currency of its own and shows the tenant default currency. Every runtime reader follows that rule: - `packages/spec/src/data/field.zod.ts`: the precision refinement judges `defaultCurrency` only under `'fixed'`. The `FieldSchema` guidance says "A field without one uses the tenant default at runtime". - `packages/services/service-analytics/src/plugin.ts` (`sourceFieldMeta`): relays `defaultCurrency` only under `'fixed'`, as landed by the runtime member of this family (#20119). - objectui's `resolveFieldCurrency`, which the field faces call: reads `defaultCurrency` only under `'fixed'`. Read from objectui's changeset `10422-currency-mode-dynamic-tenant-default.md` at its `main` (`a2f361b`). With no tenant currency set, the amount shows as a plain number. Four kinds of text still said otherwise: 1. **The spec describe** (card site 1). It called `dynamic` "user selectable". No key, stored value or runtime path offers a per-record currency choice. The value is a bare number (ADR-0104 D1), and the code lives in field config. The describe ships in the published `@objectstack/spec` and in the generated `content/docs/references/data/field.mdx` (2 rows). 2. **`content/docs/api/data-api.mdx`** (card site 3). It named "the ADR-0053 currency chain" with no mode condition. ADR-0053 is the date/datetime record and never mentions currency. 3. **service-analytics test text.** Two test files still called the chain "ADR-0053": the `preview-column-enrichment.test.ts` header table and two `it` titles, and a `query-dataset.test.ts` section comment. The #20119 PR noted the test-file labels and left them as they were. 4. **Hand-written `content/docs/**` lines** that taught `defaultCurrency` without the `fixed` condition. The census is below. ## What changes - `field.zod.ts`, **`currencyMode` describe only.** It now says what each mode does. `fixed`: the field has one currency, `defaultCurrency`. `dynamic` (the default): no field currency, amounts show in the tenant default (`localization.currency`, or a plain number when that is unset), and `defaultCurrency` is not read. Neither mode is a per-record choice. `gen:docs` regenerated the two `field.mdx` rows. No key, value, default or parse output changes. `.changeset/20126-currency-mode-describe.md` is an `@objectstack/spec` patch. - **`data-api.mdx`.** The ADR-0053 citation is gone. The callout now states the chain as it runs: measure `currency`, then the source field's `currencyConfig.defaultCurrency` only when that field is `currencyMode: 'fixed'`, then the tenant default. It also says where the chain runs. It runs on the dataset query (`POST /analytics/dataset/query`). The cube query this page documents (`POST /analytics/query`) mints no column `currency`, and `currency-mode-relay.test.ts` pins that cube-face fact. - **service-analytics, comments and test titles only.** The header table row, the two `it` titles, one in-test comment ("no field default" now reads "no fixed field currency") and the `query-dataset.test.ts` section comment now describe the chain with the fixed condition. The old ADR-0053 label is gone. No assertion or runtime line changes. - **Census fixes** (table below): `validation-rules.mdx`, `common-patterns.mdx` and `protocol/objectql/types.mdx`. ## Census: every hand-written `content/docs` line naming `defaultCurrency` (at base `84880f9`) Rule, from the card: the line must also carry `fixed`, or say it is not the displayed currency under `dynamic`. The census covers 13 lines, the same per-file counts the dispatch named (3 / 4 / 1 / 2 / 3). `releases/` is never edited. | file:line | verdict | why | |:---|:---|:---| | `data-modeling/field-types.mdx:156` | pass | the mode row states `fixed` and `dynamic` | | `data-modeling/field-types.mdx:157` | pass | "under `fixed`; not the displayed currency under `dynamic`" | | `data-modeling/field-types.mdx:160` | pass | the example carries `currencyMode: 'fixed'` | | `data-modeling/fields.mdx:77` | pass | `currencyMode: 'fixed'` line | | `data-modeling/fields.mdx:79` | pass | "the column's currency under 'fixed'" | | `data-modeling/fields.mdx:86` | pass | "not the displayed currency" under `dynamic` | | `data-modeling/fields.mdx:87` | pass | "`fixed` pins the column to `defaultCurrency`" | | `data-modeling/validation-rules.mdx:156` | changed | "3-character currency code" with no mode condition. It now adds "the field's currency under `fixed`; not the displayed currency under `dynamic`" | | `getting-started/common-patterns.mdx:53` | changed | `{ precision: 2, defaultCurrency: 'USD' }` has no mode, so it is `dynamic`, and the budget shows the tenant currency, never USD. Added `currencyMode: 'fixed'` | | `getting-started/common-patterns.mdx:82` | changed | same, in the map-format tab | | `protocol/objectql/types.mdx:297` | changed (line shape) | The YAML block already carried `currencyMode: fixed` on :296, but this line did not carry it. Added the comment "the field's currency under fixed" so the line meets the pin | | `protocol/objectql/types.mdx:308` | changed | "The currency code lives once on the field definition (`currencyConfig.defaultCurrency`)" holds only under `fixed`. It now states both modes | | `protocol/objectql/types.mdx:315` | pass | carries `fixed` (`currencyMode: fixed \| dynamic`). Left as it was, because the callout above now states the condition | Two more lines in `validation-rules.mdx` sit in the same `currency` table and belong to the same class, although neither names `defaultCurrency`: - `:155`, the `currencyMode` row, repeated card site 1's "`dynamic` (user-selectable)" wording. It now matches the describe and adds this page's validation fact: under `fixed`, an authored `precision` must agree with the currency's ISO 4217 fraction digits. - `:158` said "The per-record currency **code**", which contradicts ADR-0104 D1. It now says the code is never stored per record. Both sit inside a census-hit file and table, and both are mechanical rewrites in the wording of the accepted #20118 / #20120 pages. Pin readings after this PR, over the card's own scopes. The census script ran from the scratchpad and was not committed. - The `defaultCurrency` line rule over `content/docs/**/*.mdx` (without `releases/`): 5 failing at base, **0** at head. - `ADR-0053` within five lines of `currenc`, over `content/docs/**`, `packages/services/service-analytics/src/**` and `skills/**`: 5 hits at base (`data-api.mdx:467`, `preview-column-enrichment.test.ts:51/:282/:288`, `query-dataset.test.ts:125`) plus the skills site, and **1** at head. The one left is `skills/objectstack-ui/rules/dashboards.md:308`, which is the Tier H remainder. ## Tests and gates All readings below were taken on head `25b085386`. That head is this branch plus a merge of `main` at `e2c4e125f`, and the merge brought nothing into this PR's files. Before the gates ran, all 71 package build tasks passed (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 71/71). - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 109 commands. Each was run and its exit code recorded, and all 109 exit 0. The `--ran` reconciliation answers: "109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN". The derived set includes `check:docs`, `check:generated` (15 of 15 current), `check:api-surface`, `check:authorable-surface`, `check:doc-authoring`, `check:nul-bytes`, `check:skill-examples`, `check:dual-build-cjs-loads` and `check:type-check-debt`. - **Roster gates.** Six roster families keep their roster in a directory this PR touches, and all six exit 0: `check-changeset-fixed`, `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **This body.** Run against this text, `check-partof-closing-keyword` exits 0. - **`@objectstack/spec`.** - `pnpm --filter @objectstack/spec test`: 540 files, 15820 passed, 2 todo. - `pnpm --filter @objectstack/spec run typecheck`: exit 0. - No spec test pins the old describe text. The phrase "user selectable" appears only in the describe and its generated page. - **`@objectstack/service-analytics`.** - `pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2` on the two edited files: 2 files, 44 tests passed. - `pnpm --filter @objectstack/service-analytics run typecheck`: exit 0. - `tsc --listFiles` includes both edited test files (2 hits; the control `src/plugin.ts` has 1). - **Lint, narrowed to the 3 touched TypeScript files** (`pnpm lint` over the whole repo is CI's run): - `eslint --no-inline-config --format json` exits 0, and the JSON reports 3 files, 0 errors and 0 warnings. - `eslint --print-config` puts the files in the linted population, with 5 rules on. - `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`), so a change to these files cannot move a verdict on any untouched file. - **No ablation.** The diff changes text only: a describe, docs, comments and test titles. No assertion or runtime line changes, so no negative pin exists to ablate. ## Acceptance notes - **The `defaultCurrency` describe is left as it was.** It reads "Default or fixed currency code". The claimed surface is the `currencyMode` describe only. The generated row meets the card's pin, because it carries `fixed`, and the rewritten `currencyMode` row beside it now states the condition. "Default" can still read as a dynamic-mode default. That belongs to this family and is noted for the closure card, not changed here. - **More ADR-0053-for-currency sites exist outside the card's pin scopes.** They are the same family and are not in this PR's claimed surface: - `packages/spec/src/contracts/analytics-service.ts:80`: the published TSDoc on `AnalyticsResult.fields[].currency` reads "ADR-0053 currency chain … source-field default → tenant default", with no mode condition. It ships in the `@objectstack/spec` typings. - `packages/spec/src/data/percent-scale.ts:14`: a comment saying "mirrors the ADR-0053 currency chain". - `packages/spec/liveness/dataset.json` (the `currency` row's evidence text): "the ADR-0053 chain … the source field's `defaultCurrency`". - The card's pin scopes (`skills/**`, `content/docs/**`, `service-analytics/src/**`) do not reach `packages/spec/src/**`. These are noted for the closure card. - **A strictly line-based pin would go red on semantically correct skill lines.** `skills/objectstack-data/rules/field-types.md` lines :55, :183, :184, :191 and :192 were corrected and accepted with #20120. Each states the condition across a wrapped sentence or a two-line comment, but none carries `fixed` on its own line. Whoever writes the enumerating pin must judge a sentence or block, not a single line, or reshape those lines in the same Tier H PR. - **`data-api.mdx` makes a wider claim than currency.** The callout says `label`, `format` and `percentScale` are resolved server-side on this page's `fields[]`. That is a source read and was not measured. Both cube strategies push only `name` and `type` (`native-sql-strategy.ts:1247/:1252`, `objectql-strategy.ts:1960/:1964`), and the ADR-0021 column enrichment runs only on `queryDataset`. Only the currency sentence was in scope and was corrected. Carrier: none, noted and not filed. - **Three `query-dataset.test.ts` titles are left as they were.** They say "inherits the field defaultCurrency", "a currency field with no defaultCurrency" and "wins over the field default". They cite no ADR, so they are outside the claimed surface. The section comment above them now says that `sourceFieldMeta().defaultCurrency` is the field's fixed currency. ## Remainder of #20126 (not in this PR) - `skills/objectstack-ui/rules/dashboards.md:304`–`:308`: the unconditioned chain and the ADR-0053 citation. This is Tier H and gets its own PR, per triage execution note 1. - The enumerating pin: a test in the owning package, added by whichever PR closes the last site (no new `check-*` gate). See the line-based caveat above. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cfe2387 commit 9e7824a

9 files changed

Lines changed: 47 additions & 17 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `currencyConfig.currencyMode` description says what the runtime does with each mode, and no longer calls `dynamic` "user selectable" (#20126)
6+
7+
Clause-②: no
8+
9+
The old text read "dynamic (user selectable) or fixed (single currency)". No key, stored value or runtime path lets anyone pick a currency per record. The value is a bare number in both modes (ADR-0104 D1), and the currency code lives in field config. What the runtime does, and what the description now says:
10+
11+
- **`fixed`**: the field has one currency, `defaultCurrency`.
12+
- **`dynamic`** (the default): the field has no currency of its own. Amounts display in the tenant default currency, which is the `localization.currency` setting. When that setting is not set, amounts display as a plain number. `defaultCurrency` is not read.
13+
14+
The field faces follow this rule, and so do analytics dataset measure columns and the publish-time precision check.
15+
16+
Only the description text changes. The accepted keys and values, the defaults (`dynamic`, `CNY`) and parse output are the same as before. The generated reference page `content/docs/references/data/field.mdx` is regenerated from the new text.

‎content/docs/api/data-api.mdx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,16 @@ The **cube's metric/dimension definition** (`MetricSchema.label` / `MetricSchema
464464
([#6442](https://github.com/objectstack-ai/objectstack/issues/6442)). So a client that
465465
renders table headers or formats amounts takes `label` / `format` / `currency` /
466466
`percentScale` off the query result's `fields[]`: those are resolved server-side (the
467-
ADR-0053 currency chain, the percent-scale chain), and `format` is not reachable through
467+
currency chain below, the percent-scale chain), and `format` is not reachable through
468468
the metadata endpoint at all.
469+
470+
The currency chain runs on the dataset query (`POST /analytics/dataset/query`). A
471+
monetary measure column takes the measure's own `currency`; then, only when the source
472+
field is `currencyMode: 'fixed'`, that field's `currencyConfig.defaultCurrency`; then the
473+
tenant default currency (the `localization.currency` setting), if one is set. A
474+
`dynamic` field (the default mode) never lends its `defaultCurrency` — only `fixed` does —
475+
so its column shows the tenant default. The cube query on this page carries no column
476+
`currency`.
469477
</Callout>
470478

471479
### `GET /analytics/meta`

‎content/docs/data-modeling/validation-rules.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,10 @@ These properties apply to **all** field types and are validated by the base `Fie
152152
| `min` | `number` | — | Minimum monetary value |
153153
| `max` | `number` | — | Maximum monetary value |
154154
| `currencyConfig.precision` | `number` | `2` | Decimal places (0–10) |
155-
| `currencyConfig.currencyMode` | `enum` | `dynamic` | `dynamic` (user-selectable) or `fixed` (single currency) |
156-
| `currencyConfig.defaultCurrency` | `string` | `CNY` | 3-character currency code (ISO 4217 or crypto) |
155+
| `currencyConfig.currencyMode` | `enum` | `dynamic` | `fixed` pins the field to `defaultCurrency`, and an authored `precision` must agree with that currency's ISO 4217 fraction digits; `dynamic` displays the tenant default currency (the `localization.currency` setting). Not a per-record choice |
156+
| `currencyConfig.defaultCurrency` | `string` | `CNY` | 3-character currency code (ISO 4217 or crypto): the field's currency under `fixed`; not the displayed currency under `dynamic` |
157157

158-
**Default constraints:** Stored as a **bare number** (a finite numeric scalar — `valueSchemaFor` routes `currency` to `z.number().finite()`); there is no `{ value, currency }` envelope on the value path. The per-record currency **code** is a separate concern, carried by `currencyConfig` above. Precision defaults to 2 decimal places.
158+
**Default constraints:** Stored as a **bare number** (a finite numeric scalar — `valueSchemaFor` routes `currency` to `z.number().finite()`); there is no `{ value, currency }` envelope on the value path. The currency **code** is never stored per record: it is field configuration, carried by `currencyConfig` above. Precision defaults to 2 decimal places.
159159

160160
### `percent`
161161

‎content/docs/getting-started/common-patterns.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default defineStack({
5050
// Person references target the platform user object `sys_user`
5151
owner: { label: 'Owner', type: 'lookup', reference: 'sys_user' },
5252
due_date: { label: 'Due Date', type: 'date' },
53-
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } },
53+
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' } },
5454
}
5555
}
5656
]
@@ -79,7 +79,7 @@ export default defineStack({
7979
// Person references target the platform user object `sys_user`
8080
owner: { label: 'Owner', type: 'lookup', reference: 'sys_user' },
8181
due_date: { label: 'Due Date', type: 'date' },
82-
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, defaultCurrency: 'USD' } },
82+
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' } },
8383
},
8484
},
8585
},

‎content/docs/protocol/objectql/types.mdx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ annual_revenue:
294294
currencyConfig:
295295
precision: 2
296296
currencyMode: fixed
297-
defaultCurrency: USD
297+
defaultCurrency: USD # the field's currency under fixed
298298
```
299299

300300
**Storage:** a **bare number** — the same value shape as `number`.
@@ -305,8 +305,11 @@ annual_revenue:
305305

306306
<Callout type="warn">
307307
A currency value is **not** a `{ value, currency }` object. The currency *code*
308-
lives once on the field definition (`currencyConfig.defaultCurrency`), not on
309-
every stored value. The old per-value object shape (`CurrencyValueSchema`) was
308+
is field configuration, never stored per value: under `currencyMode: fixed` it is
309+
`currencyConfig.defaultCurrency` (the field's one currency, as `fixed` pins it);
310+
under `dynamic`, the default, it is the tenant default currency (the
311+
`localization.currency` setting) and `defaultCurrency` is not the displayed
312+
currency. The old per-value object shape (`CurrencyValueSchema`) was
310313
never consumed by the validator, the driver, or import coercion and is
311314
deprecated in the spec.
312315
</Callout>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = CurrencyConfigSchema.parse(data);
2828
| Property | Type | Required | Description |
2929
| :--- | :--- | :--- | :--- |
3030
| **precision** | `integer` | optional (default: `2`) | Decimal precision (default: 2) |
31-
| **currencyMode** | `Enum<'dynamic' \| 'fixed'>` | optional (default: `"dynamic"`) | Currency mode: dynamic (user selectable) or fixed (single currency) |
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. |
3232
| **defaultCurrency** | `string` | optional (default: `"CNY"`) | Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR) |
3333

3434

@@ -236,7 +236,7 @@ const result = CurrencyConfigSchema.parse(data);
236236
| Property | Type | Required | Description |
237237
| :--- | :--- | :--- | :--- |
238238
| **precision** | `integer` | optional (default: `2`) | Decimal precision (default: 2) |
239-
| **currencyMode** | `Enum<'dynamic' \| 'fixed'>` | optional (default: `"dynamic"`) | Currency mode: dynamic (user selectable) or fixed (single currency) |
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. |
240240
| **defaultCurrency** | `string` | optional (default: `"CNY"`) | Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR) |
241241

242242
### Nested Shape: `Field.maskingRule`

‎packages/services/service-analytics/src/__tests__/preview-column-enrichment.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
* | `label` | `measure.label` / `dimension.label` + `ctx.locale` (#6761) |
4949
* | `format` | `measure.format` |
5050
* | `builtinAggregate` | `measure.aggregate` + `measure.label == null` (#14492) |
51-
* | `currency` | ADR-0053 chain: `measure.currency` → `sourceFieldMeta().defaultCurrency` → `ctx.currency` |
51+
* | `currency` | `measure.currency` → the source field's FIXED currency (`sourceFieldMeta().defaultCurrency`, relayed only under `currencyMode: 'fixed'`) → `ctx.currency` |
5252
* | `percentScale` | `measure.derived.op === 'ratio'`, else `percentScaleOf(sourceFieldMeta())` (objectui#3136) |
5353
* | `type` | `measureResultType(measure.aggregate, sourceFieldMeta().type)` (#16101) |
5454
*
@@ -279,15 +279,15 @@ describe('#16097 — a preview response describes its measure columns like the l
279279
expect(preview.total_amount.builtinAggregate).toBeUndefined();
280280
});
281281

282-
it('currency (ADR-0053) — the sourceFieldMeta limb resolves on the preview path', async () => {
282+
it('currency — the fixed-field limb (sourceFieldMeta().defaultCurrency) resolves on the preview path', async () => {
283283
const { live, preview } = await bothPaths();
284284
expect(live.total_amount.currency).toBe('EUR');
285285
expect(preview.total_amount.currency).toBe('EUR');
286286
});
287287

288-
it('currency (ADR-0053) — the ExecutionContext limb reaches the preview path too', async () => {
288+
it('currency — the ExecutionContext (tenant default) limb reaches the preview path too', async () => {
289289
const { live, preview } = await bothPaths();
290-
// No measure currency, no field default ⇒ the tenant default on `ctx`.
290+
// No measure currency, no fixed field currency ⇒ the tenant default on `ctx`.
291291
expect(live.total_fee.currency).toBe('USD');
292292
expect(preview.total_fee.currency).toBe('USD');
293293
// A non-monetary measure never acquires one, on either path.

‎packages/services/service-analytics/src/__tests__/query-dataset.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ describe('AnalyticsService.queryDataset', () => {
122122
expect(revenueField?.format).toBe('0,0');
123123
});
124124

125-
// ── ADR-0053 currency chain (measure → field currencyConfig → tenant ctx) ──
125+
// ── currency chain (measure → the field's FIXED currency → tenant ctx) ──
126+
// `sourceFieldMeta().defaultCurrency` is the field's fixed currency: the
127+
// plugin relays `currencyConfig.defaultCurrency` only under
128+
// `currencyMode: 'fixed'`, so a `dynamic` field reaches `ctx.currency`.
126129
function pricedSvc(rows: Array<Record<string, unknown>>, sourceFieldMeta?: (o: string, f: string) => { type?: string; defaultCurrency?: string; max?: number } | undefined) {
127130
return new AnalyticsService({
128131
queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }),

‎packages/spec/src/data/field.zod.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ export const CurrencyConfigSchema = lazySchema(() => strictObject({
436436
description: 'Decimal precision (default: 2)',
437437
default: 2,
438438
}),
439-
currencyMode: z.enum(['dynamic', 'fixed']).default('dynamic').describe('Currency mode: dynamic (user selectable) or fixed (single currency)'),
439+
currencyMode: z.enum(['dynamic', 'fixed']).default('dynamic').describe('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.'),
440440
defaultCurrency: z.string().length(3).default('CNY').describe('Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR)'),
441441
}).superRefine((config, ctx) => {
442442
// #7918 (maintainer ruling 2026-08-12, Option A): an AUTHORED `precision`

0 commit comments

Comments
 (0)