Skip to content

Commit ae8e3d4

Browse files
committed
Merge #27268 head b6f152b into #27187
Brings in #27268's contract-review should-fixes. No conflicts. Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
2 parents d24c889 + b6f152b commit ae8e3d4

13 files changed

Lines changed: 105 additions & 57 deletions

‎.changeset/27184-currency-record-mode.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
'@objectstack/spec': minor
33
'@object-ui/i18n': minor
4+
'@objectstack/platform-objects': minor
45
---
56

67
feat(spec): `currencyMode: 'record'` + `currencyField` — a record-mode amount points at a sibling ISO 4217 code field (ADR-0144 D2)
@@ -17,7 +18,7 @@ fields: {
1718
}
1819
```
1920

20-
- `currencyField` is required under `record` and refused under `dynamic` and `fixed`.
21+
- `currencyField` is required under `record`, refused under `dynamic` and `fixed`, and never empty; `defaultCurrency` is refused under `record`.
2122
- 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.
2223
- `dynamic` and `fixed` configs parse exactly as before; an app that never writes `record` sees no change.
2324

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

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

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
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 not read. 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, nor 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`. |
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. |
3434

3535

3636
---
@@ -237,9 +237,9 @@ const result = CurrencyConfigSchema.parse(data);
237237

238238
| Property | Type | Required | Description |
239239
| :--- | :--- | :--- | :--- |
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 not read. 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, nor 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`. |
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. |
243243

244244
### Nested Shape: `Field.conditionalFormatting[number]`
245245

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ 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",
@@ -630,7 +630,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
630630
},
631631
"currencyConfig.defaultCurrency": {
632632
label: "Default Currency",
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."
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."
634634
},
635635
step: {
636636
label: "Step",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,19 +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."
626626
},
627627
"currencyConfig.currencyField": {
628628
label: "Campo de moneda",
629629
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."
630630
},
631631
"currencyConfig.defaultCurrency": {
632632
label: "Moneda predeterminada",
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."
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."
634634
},
635635
step: {
636636
label: "Incremento",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,19 +618,19 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
618618
},
619619
currencyConfig: {
620620
label: "通貨設定",
621-
helpText: "このフィールドの通貨(通貨フィールド、または戻り値型が「通貨」の数式フィールド)。未設定の場合は dynamic モードです。どちらのモードでも、値は単なる数値です。"
621+
helpText: "このフィールドの通貨(通貨フィールド、または戻り値型が「通貨」の数式フィールド)。未設定の場合は dynamic モードです。どのモードでも、値は単なる数値です。"
622622
},
623623
"currencyConfig.currencyMode": {
624624
label: "通貨モード",
625-
helpText: "dynamic(既定):フィールド自体は通貨を持たず、金額はテナントの既定通貨(localization.currency 設定)で表示されます。fixed:フィールドは 1 つの通貨 defaultCurrency を持ちます。"
625+
helpText: "dynamic(既定):フィールド自体は通貨を持たず、金額はテナントの既定通貨(localization.currency 設定)で表示されます。fixed:フィールドは 1 つの通貨 defaultCurrency を持ちます。record:各レコードが独自の通貨を持ち、currencyField が指すフィールドに保存されます。"
626626
},
627627
"currencyConfig.currencyField": {
628628
label: "通貨フィールド",
629629
helpText: "record モード専用で、同モードでは必須:各レコードの ISO 4217 通貨コードを保持する、このオブジェクト上のフィールド。valueDomain iso_4217_currency を宣言したテキストフィールドで、慣例名は currency_code。"
630630
},
631631
"currencyConfig.defaultCurrency": {
632632
label: "既定の通貨",
633-
helpText: "fixed モードのフィールドの通貨。3 文字の ISO 4217 コード(例:USD、EUR)で指定します。fixed モードでは必須で、自動では補われません。dynamic モードでは参照されません。"
633+
helpText: "fixed モードのフィールドの通貨。3 文字の ISO 4217 コード(例:USD、EUR)で指定します。fixed モードでは必須で、自動では補われません。dynamic モードでは参照されず、record モードでは指定できません。"
634634
},
635635
step: {
636636
label: "ステップ値",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,19 +618,19 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
618618
},
619619
currencyConfig: {
620620
label: "货币配置",
621-
helpText: "此字段使用哪种货币——货币字段,或返回类型为“货币”的公式字段。未设置时为 dynamic 模式。两种模式下值都是一个纯数字。"
621+
helpText: "此字段使用哪种货币——货币字段,或返回类型为“货币”的公式字段。未设置时为 dynamic 模式。任何模式下值都是一个纯数字。"
622622
},
623623
"currencyConfig.currencyMode": {
624624
label: "货币模式",
625-
helpText: "dynamic(默认):字段本身没有货币,金额以租户默认货币显示(localization.currency 设置)。fixed:字段只有一种货币,即 defaultCurrency。"
625+
helpText: "dynamic(默认):字段本身没有货币,金额以租户默认货币显示(localization.currency 设置)。fixed:字段只有一种货币,即 defaultCurrency。record:每条记录有自己的货币,保存在 currencyField 指向的字段中。"
626626
},
627627
"currencyConfig.currencyField": {
628628
label: "币种字段",
629629
helpText: "仅用于 record 模式,且在该模式下必填:本对象上保存每条记录 ISO 4217 币种代码的字段——一个声明了 valueDomain iso_4217_currency 的文本字段,惯例命名为 currency_code。"
630630
},
631631
"currencyConfig.defaultCurrency": {
632632
label: "默认货币",
633-
helpText: "fixed 模式字段的唯一货币,使用三个字符的 ISO 4217 代码(例如 USD、EUR)。fixed 模式下必填,不会自动填入。dynamic 模式下不读取。"
633+
helpText: "fixed 模式字段的唯一货币,使用三个字符的 ISO 4217 代码(例如 USD、EUR)。fixed 模式下必填,不会自动填入。dynamic 模式下不读取;record 模式下不允许设置。"
634634
},
635635
step: {
636636
label: "步进值",

‎packages/spec/src/data/currency-config-record-mode.test.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* amount points at a sibling ISO 4217 code field on the same object.
66
*
77
* Two doors, one per half: `CurrencyConfigSchema` refuses the half one config
8-
* can see (`record` without a pointer, a pointer under another mode), and the
8+
* can see (`record` without a pointer or with a `defaultCurrency`, a pointer
9+
* under another mode, an empty pointer), and the
910
* object's `superRefine` refuses what the pointer names (nothing, a non-text
1011
* field, a text field with no ISO 4217 value domain). A `formula` declaring
1112
* `returnType: 'currency'` carries the same config, so it meets both doors.
@@ -59,6 +60,31 @@ describe('`CurrencyConfigSchema`: `record` ⇔ `currencyField`', () => {
5960
expect(r.error.issues[0]!.message).toMatch(/^`currencyField` is read only under `currencyMode: 'record'`/);
6061
});
6162

63+
it('refuses an empty `currencyField`, at the key, in every mode', () => {
64+
for (const mode of ['record', 'dynamic', 'fixed'] as const) {
65+
const config = mode === 'fixed' ? { currencyMode: mode, defaultCurrency: 'USD', currencyField: '' } : { currencyMode: mode, currencyField: '' };
66+
const r = CurrencyConfigSchema.safeParse(config);
67+
expect(r.success, mode).toBe(false);
68+
if (r.success) continue;
69+
// Under `dynamic` / `fixed` the out-of-mode refusal is told too, at the same key.
70+
expect(r.error.issues.length, mode).toBe(mode === 'record' ? 1 : 2);
71+
expect(r.error.issues.every((i) => i.path.join('.') === 'currencyField'), mode).toBe(true);
72+
expect(r.error.issues[0]!.message).toMatch(/^`currencyField` names a field: an empty name points at nothing/);
73+
}
74+
});
75+
76+
it('refuses `defaultCurrency` under `record`, at the key, with the prescription', () => {
77+
const r = CurrencyConfigSchema.safeParse({ ...RECORD, defaultCurrency: 'EUR' });
78+
expect(r.success).toBe(false);
79+
if (r.success) return;
80+
expect(r.error.issues).toHaveLength(1);
81+
expect(r.error.issues[0]!.code).toBe('custom');
82+
expect(r.error.issues[0]!.path).toEqual(['defaultCurrency']);
83+
expect(r.error.issues[0]!.message).toMatch(/^`defaultCurrency` is not read under `currencyMode: 'record'`/);
84+
// …and `dynamic` keeps tolerating its inert code, for the rows #27157 left behind.
85+
expect(CurrencyConfigSchema.safeParse({ currencyMode: 'dynamic', defaultCurrency: 'EUR' }).success).toBe(true);
86+
});
87+
6288
it('refuses `currencyField` under the defaulted mode (no `currencyMode` written ⇒ dynamic)', () => {
6389
expect(CurrencyConfigSchema.safeParse({ currencyField: 'currency_code' }).success).toBe(false);
6490
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const fieldForm = defineForm({
107107
field: 'currencyConfig',
108108
type: 'composite',
109109
visibleWhen: "data.type == 'currency' || (data.type == 'formula' && data.returnType == 'currency')",
110-
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.',
110+
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.',
111111
fields: [
112112
{ field: 'currencyMode', type: 'select', 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.', options: [
113113
{ label: 'Dynamic — the tenant default currency', value: 'dynamic' },
@@ -116,7 +116,7 @@ export const fieldForm = defineForm({
116116
] },
117117
// ADR-0144 D2 — the record-mode pointer.
118118
{ field: 'currencyField', type: 'text', 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.' },
119-
{ field: 'defaultCurrency', type: 'text', 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.' },
119+
{ field: 'defaultCurrency', type: 'text', 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.' },
120120
],
121121
},
122122
// Every `visibleWhen` below is a MEANINGFULNESS gate, not a parse gate:

0 commit comments

Comments
 (0)