Repository navigation
Commit 62cbf9a
feat(showcase): make the invoice the live multi-currency example (#31757)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbQp4GJGprgLNVJkzvtKwfLe)_
Fixes #27197
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:** 示例应用的发票成为可运行的多币种示例:新增币种、单据日期、含税总额(按记录币种)、本位币金额和所用汇率五个字段;种子写入
EUR/JPY 两组带日期的汇率,12 张发票分别为 USD/EUR/JPY;`.env` 把本位币固定为 USD;Revenue Pulse
改为汇总本位币金额;附一个默认关闭的定时流程模板,从汇率接口拉取汇率写入 `sys_exchange_rate`;平台检查清单新增一项。
- **为什么改:** 卡片要求示例应用里有一个从字段、汇率到报表都能实际跑通的多币种例子,让客户照着开启自己的汇率同步。
- **风险与回滚:** 只改示例应用、一条测试启动助手和检查清单,不动平台代码。一个既读不到 `.env`
又没有其他本位币来源的宿主,会因本位币未设置而拒绝所有带币种的种子发票,发票列表为空(详见下文)。回滚即 revert 本 PR。
- **先看:** `examples/app-showcase/src/data/objects/invoice.object.ts`
的多币种字段段落,然后看 `src/automation/flows/exchange-rate-sync.flow.ts`。
- **两分钟验证:** `cd examples/app-showcase && pnpm exec vitest run`,预期 46
个文件全绿;或者 `pnpm dev --fresh` 后打开发票 INV-1004,应显示 €450.00 和本位币
$500.00,下方注明「Converted at 0.9 on 当天日期」。
- **需要你做的:** 无,由席位落地。
## What changes
All edits are in `examples/app-showcase/**`, one test helper and the
checklist. No platform code changes.
- **`invoice.object.ts`.** Five new fields on `showcase_invoice`, all
from the merged ADR-0144 spec:
- `currency_code` is text with `valueDomain: 'iso_4217_currency'`. It is
optional.
- `invoice_date` is the document date: a required date that defaults to
today. It is separate from `issued_on`, which stays blank on drafts.
- `amount` ("Total incl. Tax") has `currencyMode: 'record'` and
`currencyField: 'currency_code'`, plus `homeAmount: { field:
'amount_home', rateDate: 'invoice_date', rateField: 'amount_rate' }`.
Its value is a stored expression over the `total` roll-up and
`tax_rate`, rounded to two decimals.
- `amount_home` (a currency field) and `amount_rate` (a number) are
read-only. The engine writes them.
- **A2's rule: a record-mode amount with no currency stays blank.**
`amount` evaluates to null while `currency_code` is blank, so
`amount_home` and `amount_rate` stay blank too. An invoice created
without a currency, which is what the existing tests and harnesses do,
is still accepted.
- **Seeds (`seed/index.ts`).** New `exchangeRates` dataset with four
manual rows: EUR 0.92 from 2025-01-01 and 0.9 from 30 days ago; JPY
151.5 from 2025-01-01 and 149.25 from 30 days ago. Every invoice gets a
`currency_code`. INV-1004, 1009 and 1010 are EUR, INV-1007 and 1008 are
JPY, and the rest are USD. Each invoice's `invoice_date` is its
`issued_on` where one exists. INV-1008 gets two yen-priced lines, so one
JPY invoice converts a non-zero amount.
- **`.env`.** Adds `OS_LOCALIZATION_CURRENCY=USD`, which is read by `os
dev`, `os serve` and `os start`. The seeded rates are quoted from USD,
so the home currency must be USD. No app or seed door sets
`localization.currency` today: the dev seed derives it from the host's
POSIX locale, which is unset in containers.
- **A host with no .env reader and no currency gets zero invoices.** On
such a host the home currency is unset. Every seed invoice carries a
code and a non-blank amount, so each is refused
`ERR_HOME_CURRENCY_UNSET`. The seeder drops refused rows with a warning,
so the invoice list is empty and the boot still succeeds. In-process
harnesses do not read `.env`.
`packages/qa/dogfood/test/showcase-boot.ts` therefore sets
`OS_LOCALIZATION_CURRENCY ??= 'USD'` before `bootStack`; this file was
added by claim amendment 6730137663. Showcase test files that boot their
own stack create invoices with no code, so A2 keeps them green.
- **Flow template `showcase_exchange_rate_sync`.** It runs on a daily
cron (`0 6 * * *`):
- an `http` GET whose url slot reads `rate_feed_url`, a flow variable
the customer edits in Studio; the default is a USD-based public feed;
- a CEL `assignment` turns the `rates` map into rows;
- a `loop` writes each row with `create_record` on `sys_exchange_rate`
(`source: 'app'`), inside a `try_catch`, so a day that is already loaded
is skipped row by row.
The flow ships with `status: 'obsolete'` (loaded, not armed) and `runAs:
'system'`, so no boot and no CI run calls a third-party service.
- **Revenue Pulse.** The invoice measure is now `total_home_sum`, the
sum of `amount_home`, because a sum of amounts in mixed currencies means
nothing. The KPI reads "Invoiced (Home Currency)".
- **Record page (`invoice-document.page.ts`).** The page lists its
sections explicitly, so it now names the new fields: Currency and
Invoice Date join the Invoice section, and the amount, home amount and
rate join Amounts.
- **Translations.** The five field labels are added in `en` and `zh-CN`.
`amount` is labelled "Total incl. Tax" so it does not share the bundle's
"Total" with the Subtotal.
- **Checklist.** Adds the item `records-forms.multi-currency-invoice` in
`docs/qa/platform-checklist/areas/records-forms.json`.
## Tests
- No new test file. The projection, refusal and preview are pinned in
the platform by #27192 and #27196. This PR adds the example, and the
live run below verifies it.
- The existing suites ran against the new seed and the env-pinning
helper: every showcase test, and every dogfood file that boots the
showcase (counts under Verification).
## Acceptance notes
The checklist item was verified live in a run caused by this PR's
checklist edit, so there is no `qa-run` record (the `checklist-test`
skill §5). The setup was a `pnpm dev --fresh -p 4791` showcase with the
built Console, signed in as the seeded admin. `GET
/api/settings/localization` returns `currency` USD, source env, locked:
"In use by 1 object (Invoice) and 4 exchange rates".
| Clause | Verdict | Oracle evidence |
|---|---|---|
| A foreign invoice stores the rate in force on its date, and amount ÷
rate | pass | INV-1008 JPY: 2026-11-04, 237864 → `amount_rate` 149.25
(the row from 30 days ago), `amount_home` 1593.73. INV-1007 JPY:
2026-09-10 → 151.5, the 2025-01-01 row, because 149.25 starts later.
INV-1004 EUR: today, 450 → 0.9 / 500. INV-1009 EUR: 2026-08-01 → 0.92. |
| A home-currency invoice stores rate 1 and amount_home = amount | pass
| INV-1001 1749.56 → 1749.56 @ 1. INV-1003 323.94 → 323.94 @ 1. |
| No currency_code: accepted, all three blank | pass | POST without a
code → 201; `amount`, `amount_home` and `amount_rate` are all null. |
| A currency change re-projects | pass | INV-1002 PATCH `currency_code`
USD→EUR → 200: 198 @ 1 / 198 became 198 @ 0.9 / 220. |
| A date before any rate is refused, naming the currency and date, and
writes nothing | pass | POST JPY dated 2024-06-01 → 400
`ERR_EXCHANGE_RATE_MISSING`; the message names JPY and 2024-06-01. A
list by that name returns 0 rows. |
| Console: the home field is read-only with its hint, and the form
preview follows edits | pass | The INV-1004 page shows "€450.00" and
"$500.00 · Converted at 0.9 on Nov 9, 2026". The edit form shows "≈
$500.00 at 0.9 on Nov 9, 2026", then "≈ $3.02 at 149.25 …" after
switching to JPY, then "No JPY rate on or before Jun 1, 2024: saving is
refused until one is added" after setting that date. Save is refused
inline with the engine's message. |
**Screenshots** (deviceScaleFactor 2, 1280×900, kept with the run's
scripts and API readings in the project files, folder
`issue-27197/shots`):
- `detail-inv1004-eur.png`: the EUR record page.
- `detail-inv1008-jpy.png`: the JPY record page, showing ¥237,864, then
$1,593.73 converted at 149.25 on Nov 4, 2026.
- `edit-form-eur.png`, `edit-form-jpy-today.png` and
`edit-form-jpy-2024.png`: the edit form's preview line at each edit.
- `save-refused-missing-rate.png`: the inline refusal.
- `revenue-pulse.png`: the KPI showing $3,667 "Invoiced (Home Currency)"
over the last 90 days.
- `exchange-rates.png`: Setup → Exchange Rates with the four seeded
rows.
**What the example cannot show (reported to the seat, not fixed here):**
invoice lines and the Subtotal stay in dynamic mode. A child amount
cannot take its parent's record currency, and a summary over a
record-mode amount is refused (`RecordCurrencySummaryRefusedError`). So
on a EUR or JPY invoice the line prices and Subtotal display with the
home "$" while `amount` shows € or ¥. Both screenshots of the JPY
invoice show this.
## Verification
Head `c180833e`, which merges `origin/main` (5 commits, none touching a
file on this PR). The dogfood battery and the gate sweep ran on
`6d2c5b3a`, the pre-merge head with the same PR files. 11 files, +441 /
−17.
- **Showcase.** `typecheck` exits 0. `validate` exits 0, with no warning
that names a new field. `vitest run`: 46 files, 544 tests passed, re-run
after the merge of main.
- **Dogfood.** All 248 files that boot the showcase (`bootShowcase`
users and every app-showcase importer): 246 passed and 2 skipped
(env-gated); 1832 tests passed, 11 skipped; exit 0. Then the 17
invoice-writing files, re-run after the record-page and rounding commit:
17 passed, 125 tests.
- **Checklist validator.** `node scripts/check-platform-checklist.mjs`:
OK, 361 items.
- **i18n ratchet.** `check-i18n-coverage`: OK, none new.
- **Gates.** `dispatch-gates --ran`: 56 derived, 55 run, 1 NOT-MEASURED,
0 UNRUN. Every recorded exit is 0 except `check:dual-build-cjs-loads`,
which exits 3: a prerequisite is not met (unbuilt packages that this PR
does not touch), left to CI. `check-plugin-teardown-shape --self-test`
first exited 3 on the shallow clone; after fetching its pinned fixture
commit it exits 0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jS3Cr7JjyVM85apubn
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1cd0e01 commit 62cbf9a
11 files changed
Lines changed: 441 additions & 17 deletions
File tree
- docs/qa/platform-checklist/areas
- examples/app-showcase
- src
- automation/flows
- data
- objects
- seed
- system/translations
- ui
- dashboards
- datasets
- pages
- packages/qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6272 | 6272 | | |
6273 | 6273 | | |
6274 | 6274 | | |
| 6275 | + | |
| 6276 | + | |
| 6277 | + | |
| 6278 | + | |
| 6279 | + | |
| 6280 | + | |
| 6281 | + | |
| 6282 | + | |
| 6283 | + | |
| 6284 | + | |
| 6285 | + | |
| 6286 | + | |
| 6287 | + | |
| 6288 | + | |
| 6289 | + | |
| 6290 | + | |
| 6291 | + | |
| 6292 | + | |
| 6293 | + | |
| 6294 | + | |
| 6295 | + | |
| 6296 | + | |
| 6297 | + | |
| 6298 | + | |
| 6299 | + | |
| 6300 | + | |
| 6301 | + | |
| 6302 | + | |
| 6303 | + | |
| 6304 | + | |
| 6305 | + | |
| 6306 | + | |
| 6307 | + | |
| 6308 | + | |
| 6309 | + | |
| 6310 | + | |
| 6311 | + | |
| 6312 | + | |
| 6313 | + | |
| 6314 | + | |
| 6315 | + | |
| 6316 | + | |
| 6317 | + | |
| 6318 | + | |
| 6319 | + | |
| 6320 | + | |
| 6321 | + | |
| 6322 | + | |
| 6323 | + | |
| 6324 | + | |
| 6325 | + | |
| 6326 | + | |
| 6327 | + | |
| 6328 | + | |
| 6329 | + | |
| 6330 | + | |
| 6331 | + | |
| 6332 | + | |
| 6333 | + | |
| 6334 | + | |
| 6335 | + | |
| 6336 | + | |
| 6337 | + | |
| 6338 | + | |
| 6339 | + | |
| 6340 | + | |
| 6341 | + | |
| 6342 | + | |
| 6343 | + | |
| 6344 | + | |
| 6345 | + | |
| 6346 | + | |
| 6347 | + | |
| 6348 | + | |
| 6349 | + | |
| 6350 | + | |
| 6351 | + | |
| 6352 | + | |
| 6353 | + | |
| 6354 | + | |
| 6355 | + | |
| 6356 | + | |
| 6357 | + | |
| 6358 | + | |
| 6359 | + | |
| 6360 | + | |
| 6361 | + | |
| 6362 | + | |
| 6363 | + | |
| 6364 | + | |
| 6365 | + | |
| 6366 | + | |
6275 | 6367 | | |
6276 | 6368 | | |
6277 | 6369 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
2167 | 2168 | | |
2168 | 2169 | | |
2169 | 2170 | | |
| 2171 | + | |
| 2172 | + | |
2170 | 2173 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
167 | 241 | | |
168 | 242 | | |
169 | 243 | | |
| |||
0 commit comments