Skip to content

Commit e2bcd5c

Browse files
feat(objectql, cli)!: run the record-currency code backfill one organization at a time (#30737)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D5TuSiSXrriwnF8WM65LyQr)_ domain:server#1 · os-elon-musk · session_016WV4cyT9hHRjH6qFYychv1 Fixes #30674 Clause-②: yes (narrowing) — `backfillRecordCurrencyCodes` and `os migrate record-currency-codes`, given no organization over the rows of more than one organization, are refused with `400 VALIDATION_ERROR` (`field: 'organizationId'`) before any write, where they wrote the one code into every organization's codeless rows; widened beside it: the optional `organizationId` / `allOrganizations` options, the `--org` / `--all-organizations` flags, and `recomputeHomeAmounts`'s optional `organizationId` with `os migrate recompute-home-amounts --org`. Metadata-protocol: none Skills-docs: follow-up owed — skills/objectstack-data/rules/field-types.md (its "Backfill: `os migrate record-currency-codes`" hint names neither `--org` nor `--all-organizations`, so on a multi-organization database it leads to the refusal; `content/docs/deployment/cli.mdx` is updated here) Claim: 6671428099 (amended 6671472723, 6671734394) · Branch: `claude/project-thread-erl0zi` · Ruling-ref: 6670707546 (letter C, items A and 3) ## 维护者速读 - 改了什么:币种码回填按组织走。`os migrate record-currency-codes --org ID` 只写这个组织的无码行;库里这个对象的行属于多个组织、又没给 `--org` 时,命令在任何写入前拒跑(退出 1),并列出每个组织的无码行数;`--all-organizations` 是显式「一码写全部组织」;两个一起给是用法错误。重算命令 `recompute-home-amounts` 加了同样的 `--org`,不给照旧跑全部组织、不拒跑。 - 为什么改:每个组织有自己的本位币,一个 `--code` 写进所有组织的无码行,对其他组织是静默写错(ADR-0144 Open item 13,裁决 6670707546 C 的 A 项和第 3 项)。 - 风险与回滚:小。多组织库上不带范围的跑法从「写」变「拒」,这是本单的目的(BREAKING,changeset 已写迁移说明);单组织库行为和报告文字不变,已钉。回滚即撤 PR。 - 先看:`packages/objectql/src/record-currency-backfill.ts`(`resolveScope`、`backfillRecordCurrencyCodes` 的先数后写、`walkCodelessRows` 的组织过滤),`packages/cli/src/commands/migrate/record-currency-codes.ts` 的拒跑输出,`packages/objectql/src/home-amount-recompute.ts` 的 `organizationId`。 - 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run src/record-currency-backfill.test.ts -t "one organization at a time"`,预期 8 条全绿,其中「two organizations and no scope」断言零写入。 - 需要你做的:无,席位落地。 ## What changes **`@objectstack/objectql` — `backfillRecordCurrencyCodes`** (`record-currency-backfill.ts`) - New options `organizationId` and `allOrganizations`. The report gains `organizationId`, `allOrganizations` and `organizations` (the same counts per organization, ordered by id, null last). - `organizationId`: the walk and every `engine.update` run under `{ isSystem: true, tenantId: organizationId }` (ruling item A.1), and the keyset walk also carries `where: { organization_id: organizationId }`. See the first acceptance note for why both. - Neither option: the rows are walked once read-only and counted per organization. More than one named organization ⇒ `VALIDATION_ERROR` / 400, `field: 'organizationId'`, with `organizations` on the error, before any write, on a dry run as on an apply run. Otherwise the run proceeds exactly as before (an apply run walks a second time to write). - `allOrganizations`: the one code into every organization's codeless rows, under the unscoped system context (today's behaviour, now explicit). - Refused before any row is read: both options together, a blank `organizationId` (`VALIDATION_ERROR` / 400), and `organizationId` on an object that stores no `organization_id` (`INVALID_FIELD` / 400, `field: 'organization_id'`). - The report text gains a scope phrase and per-organization lines only for a scoped or all-organizations run; an unscoped single-organization run prints exactly what it printed before. - ⛔ No organization's `localization.currency` is read (ADR-0144 D8 rule 4). The card allowed showing it as a reference in the refusal; this PR does not, so the backfill keeps reading nothing about currencies but the operator's `--code`. **`@objectstack/objectql` — `recomputeHomeAmounts`** (`home-amount-recompute.ts`, ruling item 3) - New option `organizationId`: walk, the write's reads (`homeAmountReadsFor`) and every update run under `tenantId`, the walk filters on `organization_id`, and the report and the run's audit entry carry `organizationId`. Without it, every organization, as before, never refused. - Refused before any row is read: a blank organization (`VALIDATION_FAILED`, the run's existing code for a bad option) and an organization on an object that stores no `organization_id` (`INVALID_FIELD`). - The `code_blank` remedy line and the header now name `--org` instead of warning that the backfill writes every organization. **`@objectstack/cli`** - `os migrate record-currency-codes`: `--org ID`, `--all-organizations` (oclif `exclusive` pair, so both together is a usage error, exit 2, before the probe and the boot). The multi-organization refusal prints each organization's codeless count and names both flags; under `--json` the envelope carries `code`, `httpStatus` and `organizations`. The `--apply` prompt names the scope. - `os migrate recompute-home-amounts`: `--org ID`, handed to `recomputeHomeAmounts` as `organizationId`. - The command only parses and prints; scope, refusal and counts are the engine function's, so the REST door (the B card) reuses them unchanged. **Docs.** `content/docs/deployment/cli.mdx`: a "One organization at a time" paragraph and an example for `record-currency-codes`, the `--org` sentence and example for `recompute-home-amounts`, and the `code_blank` row of its skip table. ## Tests - `packages/objectql/src/record-currency-backfill.test.ts`, new describe "one organization at a time" (memory driver): two organizations and no scope ⇒ refused on dry and apply, both counts, `driver.update` never called; `organizationId` writes only A and every walk page carries the filter; under `isolated` the driver also receives `tenantId`; `allOrganizations` writes both, counted apart; both options / blank ⇒ refused before any read; a tenancy-disabled object ⇒ `INVALID_FIELD`; one organization plus rows naming none ⇒ today's behaviour and today's report text. - `packages/objectql/src/engine-home-amount-projection.test.ts`, new describe "recompute: an organization filter": only B's row rewritten, A's stored amount untouched, audit entry carries the organization; without it both; refusals before any read. - `packages/cli/src/commands/migrate/record-currency-codes.test.ts`: the flags reach the engine; the pair is a usage error with nothing probed, booted or walked; the real engine's multi-organization refusal reaches the `--json` envelope with both counts and exits 1; the human face lists both and names both flags. `recompute-home-amounts.test.ts`: `--org` reaches the engine. - `packages/qa/dogfood/test/record-currency-backfill-organizations.test.ts` (new) on better-sqlite3, sqlite-wasm and PostgreSQL, under the default `single` posture: the refusal with zero writes, `--org` A then B with B's own code, all organizations, one organization unchanged. `home-amount-projection.test.ts`: the recompute's organization filter on the same three cells. - `packages/objectql/src/federated-injected-column-readers.test.ts`: the backfill's new `organization_id` read is registered as a `skips` site (its guard reds on an unregistered reader). ## Acceptance notes - **The system context alone does not scope a read under `single`.** `engine-driver-read-scope.test.ts` pins that `{ isSystem: true, tenantId }` reaches the driver as an organization predicate only under a walled posture (`isolated` / `group`); under `single` the verdict is `none`. A CLI boot resolves the posture from its own shell (`OS_TENANCY_POSTURE`, else `single`), which need not be the server's. So both runs also filter on the row's own `organization_id`. Measured by ablation below: without the filter the dogfood scoped pin goes red on all three drivers. - **"More than one organization" is read from the object's own rows**, the distinct non-null `organization_id` values the walk meets, not from `sys_organization`. An organization with no row of the object has nothing the code could be wrong for, and rows that name no organization never make a run multi-organization (they ride along with an unscoped run as before). - The organization's `localization.currency` is not shown beside the refusal's counts (the card made it optional); a door that wants it can add it from `homeAmountReadsFor` without touching this function. - **Commit trailers.** The four commits on this branch are WIP commits without the `Claude-Session:` / `Co-authored-by:` trailer pair. The branch is already pushed and is not force-pushed, so the pair belongs in the squash message at landing. - **Not re-exported.** The new types `RecordCurrencyBackfillOrganization` and `RecordCurrencyBackfillOrganizationsRefusal` are exported from the module but not from the `@objectstack/objectql` barrel, which names its exports one by one and lies outside this card's file surface. The CLI recognises the refusal by its `organizations` array. The B card (the REST door) can add both to the barrel when it needs them. ## Verification <details><summary>Local tests (head f7b7814)</summary> - `pnpm --filter @objectstack/objectql exec vitest run` (whole package): 556 files, 11079 tests passed. - `pnpm --filter @objectstack/objectql exec vitest run src/record-currency-backfill.test.ts src/engine-home-amount-projection.test.ts src/federated-injected-column-readers.test.ts`: green. - `pnpm --filter @objectstack/cli exec vitest run --project integration src/commands/migrate/record-currency-codes.test.ts src/commands/migrate/recompute-home-amounts.test.ts`: 2 files, 26 tests passed (the migrate command tests live in the `integration` project; `--project unit` selects nothing). - `pnpm --filter @objectstack/dogfood exec vitest run test/record-currency-backfill-organizations.test.ts test/home-amount-projection.test.ts` with `OS_TEST_POSTGRES_URL` set (local PostgreSQL 16): 2 files, 57 tests passed, the PostgreSQL cells included. - Type-check of `@objectstack/objectql`, `@objectstack/cli` and `@objectstack/dogfood`: exit 0. - `pnpm lint`: exit 0. </details> <details><summary>Ablations</summary> Each mutation was made on disk, measured red, restored, and measured green again. 1. The refusal (`named.length > 1` changed to `> 999` in `backfillRecordCurrencyCodes`): the objectql pin "two organizations and no scope" goes red; restored green. 2. The organization filter (the `where` on the scoped walk removed): two objectql pins go red. Then, because dogfood resolves `@objectstack/objectql` through its `dist`, the same mutation was rebuilt into `dist`, `scripts/ablation-dist-preflight.mjs` confirmed the mutation was present in the built output, and the scoped dogfood pin went red on all three cells (better-sqlite3, sqlite-wasm, PostgreSQL). After the restore and rebuild, the preflight reported the marker absent, the tree was clean, and all cells were green. A first attempt at ablation 2 mutated only `src` and the dogfood leg stayed green. That was a false green: the dogfood leg never read the mutated source. It is not counted above. The rebuild and preflight above replace it. </details> <details><summary>Derived gates (dispatch-gates --commands, reconciled with --ran)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 94 families on this tree. Each was run with its exit code captured before any pipe (`gates-final.txt`). ``` Run reconciliation — 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED (a DERIVED zero — all 94 recorded an exit code and none of them is 3). ``` On the first pass, three families exited 3 (PREREQUISITE NOT MET), which are not findings: - `check-plugin-teardown-shape --self-test` needed a deeper clone. - `check:skill-examples` and `check:dual-build-cjs-loads` needed built `dist/` for client-react, studio, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis and service-knowledge. After `git fetch --unshallow origin main` and a build of those packages, all three were rerun and exited 0. The CI families outside the derivation are read from the PR's check-runs, not rerun here. </details> <details><summary>Full dogfood battery (final head)</summary> `f7b7814e` · `bash scripts/pm/os-verify-lock.sh -c "pnpm --filter @objectstack/dogfood test"` (no shard, no path), after the `@objectstack/dogfood^...` closure build, with `OS_TEST_POSTGRES_URL` set to a local PostgreSQL 16: ``` Test Files 422 passed | 3 skipped (425) Tests 3547 passed | 17 skipped (3564) os-verify-lock: VERDICT command-exit 0 · held the lock 1466s (24m26s) ``` No failures. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` untouched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [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: n/a. - [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_016WV4cyT9hHRjH6qFYychv1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e00bcc6 commit e2bcd5c

14 files changed

Lines changed: 904 additions & 39 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/cli': patch
4+
---
5+
6+
feat(objectql, cli)!: the record-currency code backfill runs one organization at a time, and the home-amount recompute takes the same organization filter
7+
8+
Clause-②: yes (narrowing) — `backfillRecordCurrencyCodes` (and `os migrate record-currency-codes`) given no organization over the rows of more than one organization is refused with `400 VALIDATION_ERROR` (`field: 'organizationId'`) before any write, where it wrote the one code into every organization's codeless rows; widened beside it: the optional `organizationId` and `allOrganizations` options and the `--org` / `--all-organizations` flags, and `recomputeHomeAmounts`'s optional `organizationId` with `os migrate recompute-home-amounts --org`.
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) no key of any schema is added, removed or re-shaped and no stored row is converted: an operator's run of a data command over several organizations is refused until it names its scope, which is a choice of currency per organization made by hand, not a mechanical conversion -->
12+
13+
**BREAKING** for an operator who runs `os migrate record-currency-codes` (or calls `backfillRecordCurrencyCodes`) without naming an organization on a database whose rows of that object belong to more than one organization.
14+
15+
- **Why (ADR-0144 Open item 13).** Each organization has its own home currency (`localization.currency`), so the one code a run writes is right for the rows of at most the organizations that use it. Before this release the run wrote it into every organization's codeless rows, silently wrong for the others.
16+
- **One organization.** `--org ID` (`organizationId`) fills only that organization's rows: the walk and every write run under `{ isSystem: true, tenantId: ID }`, and the walk also filters on each row's own `organization_id`, because the system context alone narrows the driver only under a walled tenancy posture. The report counts per organization (`organizations`).
17+
- **Refused, and what it says.** With neither `--org` nor `--all-organizations`, the rows are first counted read-only; when they belong to more than one organization the run (a dry run as well as `--apply`) exits 1 before any write. The refusal lists each organization's codeless row count, and under `--json` carries them in `organizations`. The code is still never derived: no organization's `localization.currency` is read to pick one (ADR-0144 D8 rule 4).
18+
- **Every organization, explicitly.** `--all-organizations` (`allOrganizations: true`) writes the one code into every organization's codeless rows, as an unscoped run did before. `--org` with `--all-organizations` is a usage error; the engine refuses both options together with `400 VALIDATION_ERROR`.
19+
- **Unchanged.** A database whose rows of the object belong to one organization, or name none, runs exactly as before, with the same report text.
20+
- **The recompute.** `os migrate recompute-home-amounts --org ID` (`recomputeHomeAmounts`'s `organizationId`) limits the run to that organization's rows, read and written as it, and records the organization in the run's audit entry. Without it the run still covers every organization, each in its own home currency, and is never refused for that: it writes no value the operator chose. Its `code_blank` remedy now names `--org`.
21+
- **Who moves, and the fix.** A script that ran the backfill once over a multi-organization database runs it once per organization with `--org ID` and the code that organization's rows are in, or adds `--all-organizations` when one code really is right for all of them.

‎content/docs/data-modeling/currency.mdx‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,12 @@ a grouped aggregate, and sits outside every filter on the pair until it holds a
141141
The code is the one you give with `--code`, and it is never derived: not from the
142142
`defaultCurrency` the field had under `fixed`, not from the organization's currency.
143143
One run writes one code, so if the stored amounts are in more than one currency, split
144-
those rows with your own writes first. See
144+
those rows with your own writes first. Each organization has its own home currency, so
145+
on a database whose rows belong to more than one organization, run it once per
146+
organization with `--org <id>` and the code that organization's rows are in: a run
147+
naming no organization there is refused before any write and lists each organization's
148+
codeless row count, and `--all-organizations` writes the one code into every
149+
organization's rows when it really is right for all of them. See
145150
[`os migrate record-currency-codes`](/docs/deployment/cli#os-migrate-record-currency-codes).
146151

147152
### Aggregating a record-mode amount
@@ -362,8 +367,8 @@ lists every row that changes, with its stored and new values.
362367
of 1, a later rate or a zero. The report names each one and why: no rate in force on its
363368
document date (add the rate), an amount with no currency code (run
364369
[`os migrate record-currency-codes`](#switching-a-field-that-already-holds-rows) first: no
365-
rate can help a row that names no currency; it writes its one code into every
366-
organization's codeless rows, so check the per-organization count first), no document date, or no home currency. Until
370+
rate can help a row that names no currency; give it `--org` for each organization's
371+
code), no document date, or no home currency. Until
367372
a re-run converts them, a total over the home field leaves those rows out, so the command
368373
keeps exiting 1 while any is listed, and the applied run's audit entry records them. The
369374
full reference is [`os migrate recompute-home-amounts`](/docs/deployment/cli#os-migrate-recompute-home-amounts).

‎content/docs/deployment/cli.mdx‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,7 @@ covers the switch). This command writes the code into those rows.
16231623
```bash
16241624
os migrate record-currency-codes --object opportunity --field amount --code USD # Dry run: counts the rows, writes none
16251625
os migrate record-currency-codes --object opportunity --field amount --code USD --apply # Write the code (prompts)
1626+
os migrate record-currency-codes --object opportunity --field amount --code EUR --org org_emea --apply # One organization
16261627
os migrate record-currency-codes --object opportunity --field amount --code USD --apply --yes --json # CI / scripts
16271628
```
16281629
@@ -1634,6 +1635,21 @@ anything else. A row that holds a code is never touched, whatever the code, and
16341635
with no amount owes none. If the stored amounts are in more than one currency, split
16351636
them with your own writes first: nothing here can tell those rows apart.
16361637
1638+
**One organization at a time.** Each organization has its own home currency, so one
1639+
code is right for the rows of at most the organizations that use it. `--org <id>` fills
1640+
only that organization's rows: the walk and every write run as that organization, and
1641+
the walk filters on each row's own `organization_id`. When the object's rows belong to
1642+
**more than one organization** and you give neither `--org` nor `--all-organizations`,
1643+
the run (a dry run as well as `--apply`) is refused with `VALIDATION_ERROR` / 400 and
1644+
exit 1 before any row is written, and the refusal lists each organization's codeless
1645+
row count; under `--json` they are in its `organizations` array. Re-run with `--org`
1646+
once per organization, each with the code its rows are in. `--all-organizations` is
1647+
the explicit way to write the one code into every organization's codeless rows, and
1648+
the report then counts each organization apart. `--org` and `--all-organizations`
1649+
together are a usage error. When the rows belong to one organization (or name none),
1650+
the run behaves as it always did. The command never reads an organization's
1651+
`localization.currency` to pick a code for you.
1652+
16371653
**A bad `--code` is refused before anything is opened.** A code that is not an
16381654
uppercase ISO 4217 code (`usd`, `EURO`) is refused with `INVALID_FIELD` / 400 and
16391655
exit 1 before the database is probed, the `--apply` prompt is shown or the app is
@@ -1658,6 +1674,7 @@ report.
16581674
```bash
16591675
os migrate recompute-home-amounts --object invoice # Dry run: reports, writes none
16601676
os migrate recompute-home-amounts --object invoice --since 2026-01-01 # Documents dated on or after Jan 1 only
1677+
os migrate recompute-home-amounts --object invoice --org org_emea # One organization's rows only
16611678
os migrate recompute-home-amounts --object invoice --field amount --apply # Write (prompts)
16621679
os migrate recompute-home-amounts --object invoice --apply --yes --json # CI / scripts
16631680
```
@@ -1683,7 +1700,10 @@ one amount; without it, every amount of the object that declares a home amount i
16831700
recomputed. `--since YYYY-MM-DD` keeps the rows whose **document date** (the field
16841701
`homeAmount.rateDate` names, the date that picks the rate) is on or after that day, a
16851702
`datetime` read as its day in the organization's timezone; a row with no document date is
1686-
outside every `--since` window. A malformed `--since` is refused with `VALIDATION_FAILED` /
1703+
outside every `--since` window. `--org <id>` limits the run to one organization's rows,
1704+
read and written as that organization; without it the run covers every organization,
1705+
each in its own home currency, and is never refused for that (it writes no value you
1706+
chose). A malformed `--since` is refused with `VALIDATION_FAILED` /
16871707
400 before the database is opened; an object the deployment does not declare
16881708
(`OBJECT_NOT_FOUND`), or one or a `--field` that declares no home amount (`INVALID_FIELD`),
16891709
is refused after the boot and before any row is read.
@@ -1693,7 +1713,7 @@ is refused after the boot and before any row is read.
16931713
| Reason | The row | Fix, then re-run |
16941714
|:---|:---|:---|
16951715
| `rate_missing` | No rate for its currency is in force on its document date (the report names both). | Add the rate. |
1696-
| `code_blank` | An amount with no currency code: stored before the field switched to record mode. | [`os migrate record-currency-codes`](#os-migrate-record-currency-codes). It writes its one code into the codeless rows of **every** organization, so check the report's `code_blank` count per organization first. |
1716+
| `code_blank` | An amount with no currency code: stored before the field switched to record mode. | [`os migrate record-currency-codes`](#os-migrate-record-currency-codes) with `--org`, once per organization, each with the code its rows are in; the report's `code_blank` count per organization says which ones owe codes. |
16971717
| `document_date_blank` | A foreign-currency amount with no document date. | Fill the document date. |
16981718
| `home_currency_unset` | Its organization has no `localization.currency`. | Set **Localization → Default currency**. |
16991719
| `unconvertible` | A code, amount or document date the write itself refuses. | Correct the value. |

‎packages/cli/src/commands/migrate/recompute-home-amounts.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ describe('os migrate recompute-home-amounts', () => {
133133
expect(json()).toMatchObject({ apply: false, report: { changed: 1, written: 0 } });
134134
}, RUN_TIMEOUT);
135135

136+
it('--org hands that organization to recomputeHomeAmounts as its filter; without it, none (every organization)', async () => {
137+
await MigrateRecomputeHomeAmounts.run(['--json', '--object', 'invoice', '--org', 'org_b'], { root: CLI_ROOT });
138+
expect(vi.mocked(recomputeHomeAmounts).mock.calls[0]![2]).toMatchObject({ object: 'invoice', organizationId: 'org_b' });
139+
await MigrateRecomputeHomeAmounts.run(['--json', '--object', 'invoice'], { root: CLI_ROOT });
140+
expect(vi.mocked(recomputeHomeAmounts).mock.calls[1]![2]).toHaveProperty('organizationId', undefined);
141+
}, RUN_TIMEOUT);
142+
136143
it('--apply --yes writes, on the plain boot', async () => {
137144
await MigrateRecomputeHomeAmounts.run(['--json', '--apply', '--yes', '--object', 'invoice'], { root: CLI_ROOT });
138145
expect(vi.mocked(recomputeHomeAmounts).mock.calls[0]![2]).toMatchObject({ apply: true });

‎packages/cli/src/commands/migrate/recompute-home-amounts.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ async function confirm(question: string): Promise<boolean> {
6060
* and keep the command's exit code at 1 until a re-run converts them
6161
* (ADR-0144 Open item 8). Idempotent: a re-run with nothing listed is the
6262
* verification. No `sys_migration` flag is recorded: nothing gates on the run.
63+
*
64+
* `--org <id>` limits the run to one organization's rows, the same flag
65+
* `os migrate record-currency-codes` takes (ruling 6670707546 item 3), handed
66+
* to `recomputeHomeAmounts` as its organization filter. Without it the run
67+
* covers every organization, each in its own home currency, and is never
68+
* refused for it: a recompute writes no value the operator chose.
6369
*/
6470
export default class MigrateRecomputeHomeAmounts extends Command {
6571
static override description =
@@ -70,6 +76,7 @@ export default class MigrateRecomputeHomeAmounts extends Command {
7076
static override examples = [
7177
'$ os migrate recompute-home-amounts --object invoice',
7278
'$ os migrate recompute-home-amounts --object invoice --since 2026-01-01',
79+
'$ os migrate recompute-home-amounts --object invoice --org org_emea',
7380
'$ os migrate recompute-home-amounts --object invoice --field amount --apply',
7481
'$ os migrate recompute-home-amounts --object invoice --apply --yes --json',
7582
];
@@ -89,6 +96,10 @@ export default class MigrateRecomputeHomeAmounts extends Command {
8996
'date, YYYY-MM-DD. A datetime document date is read as its calendar day in the organization\'s timezone; ' +
9097
'a row with no document date is outside every --since window',
9198
}),
99+
org: Flags.string({
100+
description:
101+
'Only this organization\'s rows (its id). Default: every organization, each in its own home currency',
102+
}),
92103
apply: Flags.boolean({
93104
description: 'Write the recomputed home amounts and rates (default is a read-only dry run)',
94105
default: false,
@@ -168,8 +179,8 @@ export default class MigrateRecomputeHomeAmounts extends Command {
168179
}
169180
const ok = await confirm(
170181
chalk.bold(
171-
`\nRewrite the stored home amounts of ${flags.object}${flags.field ? `.${flags.field}` : ''} at the rates ` +
172-
'in force now, on this database? [y/N] ',
182+
`\nRewrite the stored home amounts of ${flags.object}${flags.field ? `.${flags.field}` : ''}` +
183+
`${flags.org !== undefined ? ` in organization ${flags.org}` : ''} at the rates in force now, on this database? [y/N] `,
173184
),
174185
);
175186
if (!ok) {
@@ -228,6 +239,7 @@ export default class MigrateRecomputeHomeAmounts extends Command {
228239
object: flags.object,
229240
field: flags.field,
230241
since: flags.since,
242+
organizationId: flags.org,
231243
apply,
232244
maxRecords: flags['max-records'],
233245
source: 'os migrate recompute-home-amounts',

0 commit comments

Comments
 (0)