Repository navigation
feat(platform-objects): sys_exchange_rate — an organization's dated exchange rates, admin-written and audited, with a Setup list under Localization - #27246
Conversation
…up list under Localization (WIP) Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…arget classification (WIP) Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…, its list and the Setup entry Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…nership roster Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…nancy census counts it Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 48 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8ccbc667e5f57dc51051bedd83573593a3b17536 && git checkout 8ccbc667e5f57dc51051bedd83573593a3b17536
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9ff8154cf694c616de5d467b35af21e037de1509 4bc2a793aa4d1a34adc64ef32f83f52b8539777b && git checkout -B drift-repro 9ff8154cf694c616de5d467b35af21e037de1509 && git merge --no-ff 4bc2a793aa4d1a34adc64ef32f83f52b8539777b
node scripts/docs-audit/affected-docs.mjs --json 9ff8154cf694c616de5d467b35af21e037de1509
|
…ency ADR by its card, not an unwritten number Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
… 24 (ruling 6469382727) Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
…platform-objects Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
The new tenant-scoped platform table carries organization_id, so `os migrate organization-ownership` refused every plan as carrying an uninventoried platform table. It takes fate 3 (attribution, no anchors: the Default Organization under single, otherwise reported), as the other organization-authored tables added since the first census do. Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
SETUP_NAV_CONTRIBUTIONS gains nav_exchange_rates, so the composition's Setup tree grows from 31 entries to 32. Claude-Session: https://claude.ai/code/session_01Utp6CZ8iiopaXHsTY7SiRf Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Triage seat, under gap rule (b) (#27062): the PR has been open since 12:33Z with no record or claim on any head (named as due in R731). The review runs at Generated by Claude Code |
Contract reviewServed-tier: Governing shape: ADR-0144 (PR #27239, head ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by Bill · project thread
Fixes #27185
⛔ not released alone — pairs with #27192 (R2); merges in the R2 window
Clause-②: no
Metadata-protocol: none
维护者速读
sys_exchange_rate(币种 ISO 4217、汇率精确小数(最多 18 位小数,见下方说明)、生效日期、来源 manual/import/app),同组织内 (币种, 生效日期) 唯一;Setup → Configuration 在 Localization 下面加「Exchange Rates」列表页,页面上写明一次汇率方向;文档一行讲财务怎么录一个月的汇率。packages/platform-objects/src/system/sys-exchange-rate.object.ts(唯一键的'organization'拼法、rate的scale/min与方向句);src/plugin.ts(注册进第一个 manifest,走项目库);src/apps/setup-nav.contributions.ts(菜单位置)。pnpm --filter @objectstack/platform-objects exec vitest run src/system/sys-exchange-rate.object.test.ts,预期 15 个测试全绿。What changes
sys_exchange_rate(packages/platform-objects/src/system/sys-exchange-rate.object.ts),managedBy: 'system-data':currency— text,valueDomain: 'iso_4217_currency', required.rate—number(the platform's exact-decimal column, neverfloat),scale: 18,min= 10⁻¹⁸ (strictly positive), required. Why 18, not 12: see Acceptance notes.effective_date— date, required.source— selectmanual/import/app, defaultmanual.(currency, effective_date)spelledunique: 'organization'(ADR-0120 D1), so two organizations each hold their own EUR rate for a day.organization_idis injected and the Layer 0 wall scopes it.enable.trackHistory: trueplus per-fieldtrackHistoryon the four ruled columns; the object is not on plugin-audit's skip list, so every write lands insys_audit_log.organization_admin's'*'grant reaches it (notprivate, norequiredPermissions);member_defaultnames no business object, so members neither read nor write it (ruling parameter 2).EXCHANGE_RATE_DIRECTION(exported) is theratefield's description and theall_rateslist view's description, which the console renders on the list page.EXCHANGE_RATE_SCALEexported beside it.PlatformObjectsPluginregisters it in its first manifest (project database, no routing triple), besidesys_migration/sys_migration_journal/sys_secret: its reader is the ObjectQL engine's write path, present in every composition.nav_exchange_ratesingroup_configurationdirectly undernav_settings_localization, openingall_rates.en/zh-CN/ja-JP/es-ES(the three translated locales hand-written; bundles regenerated withcheck-i18n-bundles.mjs --write, extract config gains the object).content/docs/ui/setup-app.mdx— the Configuration row and one entry saying how finance records a month's rates..changeset/27185-exchange-rate-table.md,@objectstack/platform-objectspatch.Why
Multi-currency shape C′ (ruling 6465431966 on #27119; epic #8345, M1-1): a record-mode amount is converted to the home currency at write from the rate in force on its document date. This is that rate table. Parameters 2 (admin-only) and 3 (1 home =
rateforeign, so home = amount ÷rate) are applied as ruled; parameter 4 as recorded in Acceptance notes.Tests
src/system/sys-exchange-rate.object.test.ts(15 cases) pins the declaration each enforcing mechanism reads, on the built value: one unique key on(currency, effective_date)equal to'organization';resolveInjectedSystemColumnsplansorganization_id; notprivateand norequiredPermissions(the org-admin wildcard reaches it);resolveCrudAffordancesgives New/Edit/Delete and no import;trackHistoryon object and columns;iso_4217_currencywith the write predicate's member / non-member / lowercase answers;rateisnumberwithscale18 andmin10⁻¹⁸; the direction sentence on field and view; the nav entry sits right after Localization and names a declared view.plugin.test.ts(registration roster),platform-objects.test.ts(nav target classified as owned and listable),bundle-ownership.test.ts(bundle roster).Acceptance notes
default-permission-sets.ts; plugin-auditAUDIT_EXCLUDED_OBJECTS).platform-objectscannot import plugin-security or plugin-audit (they depend on it), and the claimed file surface stops at this package, so no live admin-201 / member-403 / duplicate-409 / cross-org-201 run is recorded: NOT MEASURED, carrier examples + QA: showcaseinvoicegoes multi-currency (code field, record amount, home field, document date), seeded rates, a rate-sync flow template, a platform-checklist item and a browser dogfood run (multi-currency M3-3, epic #8345) #27197 (M3-3 dogfood seeds rates and drives the browser).system-data's default) until import: verifysys_import_jobservessys_exchange_rate(CSV of currency, rate, effective_date); add the importer only if it does not (multi-currency M1-2, epic #8345) #27190 verifiessys_import_jobserves this object; turning it on isuserActions: { import: true }there.scale: 18satisfies both: 12 places for every rate, and 12 significant digits for every rate down to 10⁻⁶ (e.g. 1 VND = 0.0000393 USD needs 17 places). The column is the platform'sDECIMAL(65,30)whateverscalesays (NUMERIC_COLUMN_REPRESENTATION), so settling the open item later editsEXCHANGE_RATE_SCALEand moves no data.rate(ADR-0144). Card objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192's body writes the projection as amount × rate; that card should follow the ruling.numberas REAL (table.decimalcompiles tofloaton SQLite), and every dialect reads anumberback as a JS double (about 15–17 significant digits). A rate written at the full 18 places beyond that is not round-tripped exactly; objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 already owns pinning SQLite rounding.scripts/platform-object-tenancy-census.json, regenerated withnode scripts/platform-object-tenancy-census.mjs --writebecause the new tenant-scoped object joins the census population (87 → 88 registered, 44 → 45 in reach). Derived artefact, no hand edit.localization.currencyonce asys_exchange_raterow or ahomeAmountdeclaration exists; Setup shows what uses it (multi-currency M1-3, epic #8345) #27198).Verification
Package test + typecheck (lock VERDICT)
Derived gates (dispatch-gates --commands, --ran reconciliation)
Human Minimum
packages/specholds no runtime logic. (No spec change.)pnpm lintis CI's.main, one pull request.packages/specnot touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code