Skip to content

examples + QA: showcase invoice goes 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

Open

Description

@objectstack-fleet

Blocked-by: #27192
Blocked-by: #27196

Filing gate ③ (maintainer-directed). Ruling 6465431966 on #27119 (batch #421 item 2, Q1 = 乙-现在, maintainer 「同意立专题卡」 2026-10-28): the multi-currency epic resumes on #8345 in shape C′. Part of #8345. Filed by the director seat (session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). ⛔ Not a claim. Workload row and precedents: 6464936321; design: 6464438764.

Ruling-ref: 6465431966

维护者速读

  • 改了什么: showcase 的 invoice 加币种码、record 金额、影子列、单据日期;种子数据设组织币种并塞几条汇率;附一条「定时拉汇率写入 sys_exchange_rate」的流程模板(示例,不是平台内置源);平台测试清单加一项;跑一次浏览器 dogfood。
  • 为什么改: 是整个功能的活证明,也是 AI 作者照抄的样板。
  • 风险与回滚: 示例与 QA,无契约。
  • 先看: examples/app-showcase/src/data/objects/invoice.object.ts、种子、docs/qa/platform-checklist/。
  • 两分钟验证: 把组织币种改成 USD 再跑一遍,发票对象零改动仍正确。
  • 需要您做的: 无。

Task

  • Metadata + seeds (rates for two foreign currencies).
  • Flow template: timer + http callout + create rows; URL is a setting the customer changes.
  • Checklist item + run record (checklist-test).

Lane: devx · Size: M · Review: default. Clause-②: no · Metadata-protocol: none.

Epic: #8345 (parent; the epic PM dispatches pm:epic children; domain:spec children go to the spec seat's queue). Dedupe: #8345 thread, #27119 (ruled), the workload list 6464936321; no open card carries this slice.

Activity

  1. added
    enhancementNew feature or request
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    pm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queue
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Oct 28, 2026
  2. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: released to the lane under #31502 A′ · pm:epic removed, stays pm:blocked · 2026-11-08T16:15Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. removed
    pm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queue
    on Nov 8, 2026
  4. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo

    Unblocked · 2026-11-09T01:01Z · ⛔ Not a claim. Both Blocked-by: lines are closed: #27192 on 2026-10-31, and #27196 today. #27196 merged via PR #31638 (2e33533e) and PR #31724 (2a32bd45) on main. So pm:blocked → pm:queue, and the card stays in domain:devx. What it can now use:

    • the read-only home field with its "Converted at RATE on DATE" hint;
    • the Studio homeAmount pickers;
    • the live "≈ home amount" preview.

    The dispatching seat re-checks the file surface against the merged ref.

  5. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    pm:queueReady for the PM dispatch loop
    on Nov 9, 2026
  6. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6

    Claim: PM loop round 8 · 2026-11-09T01:21Z
    Session: session_01QsR2jS3Cr7JjyVM85apubn (card thread, dev) · seat session_01QsR2jTo666XXx9e1Gm21C6
    Account: zhuangjianguo
    Branch: claude/project-thread-jdqqde
    Worktree: objectstack-issue-27197
    Domain: domain:devx
    Seat: domain:devx#2
    File surface: examples/app-showcase/** (the invoice object, seeds, the exchange-rate flow template, and their generated translations if any), docs/qa/platform-checklist/** and its run record. ⛔ No platform package code, no packages/spec/**, no ui/.github/**.
    Container & model: M, mode:thread, model: default tier (no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the showcase's multi-currency example | invoice has no currency code, home amount or document date, though #27192 and #27196 shipped them | AI authors who copy the showcase, and the platform checklist
    Thread-read: 6729749606
    Premise: holds; both Blocked-by: cards are closed (6729749606), and the step-1 comment re-reads the merged declarations
    Serial constraints cleared: none open. No other devx PR touches examples/app-showcase/**.

    Scope: step 1 (declarations, seeds, flow shape, checklist item) is posted before code. Then one PR. The PR says Fixes #27197. Brief: /mnt/project-files/issue-27197/brief.md. Summons: ruling 6465431966 (card body), unblocked by 6729749606.

  7. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · zhuangjianguo · session_01QsR2jS3Cr7JjyVM85apubn

    Step 1 · declarations, seeds, flow shape, checklist item · read at origin/main f2abd476

    ⛔ Not a claim (Claim 6729986143 holds). No code written. One item below needs the seat's call before code (§2, home currency at seed time).

    1. Field declarations on showcase_invoice (read against field.zod.ts CurrencyConfigSchema / HomeAmountSchema and object.zod.ts refuseDishonestRecordCurrencyField / refuseDishonestHomeAmount)

    currency_code: Field.text({ label: 'Currency', required: true, maxLength: 3, valueDomain: 'iso_4217_currency' }),
    invoice_date:  Field.date({ label: 'Invoice Date', required: true, defaultValue: 'NOW()' }),
    amount: Field.currency({
      label: 'Invoice Total',
      // stored, recomputed on every write (summary recompute is a by-id update, so it re-runs)
      expression: cel`record.total * (1 + record.tax_rate / 100)`,
      currencyConfig: {
        currencyMode: 'record',
        currencyField: 'currency_code',
        homeAmount: { field: 'amount_home', rateDate: 'invoice_date', rateField: 'amount_rate' },
      },
    }),
    amount_home: Field.currency({ label: 'Total (Home Currency)', readonly: true }),   // dynamic mode, no expression
    amount_rate: Field.number({ label: 'Exchange Rate Used', readonly: true }),       // no scale (spec floor 12)
    • Spec keys: currencyConfig.currencyMode: 'record' + currencyField (ADR-0144 D2), currencyConfig.homeAmount.{field, rateDate, rateField} (D5), valueDomain: 'iso_4217_currency', readonly: true on both projected fields, defaultValue: 'NOW()' (a legal date token).
    • Names follow the design row M3-3 (6464936321): currency_code, amount, amount_home, invoice_date, plus amount_rate, which homeAmount requires since it was made mandatory.
    • invoice_date is a new field, not issued_on. issued_on is blank on drafts (requiredWhen sent/paid), and the engine refuses a foreign amount with a blank document date (home-amount-projection.ts, "required to convert"). A required, today-defaulted document date keeps drafts saveable. The alternative is to reuse issued_on and give it the default; that changes an existing demo field, so I did not pick it.
    • amount is the tax-inclusive grand total, which today is shown only live in the entry form. It is computed from the total roll-up, so it cannot drift from the lines. I will measure the null total case on insert before relying on it.
    • What stays dynamic: the line unit_price / amount and the total Subtotal. A child cannot point currencyField at its parent's code, and a summary over a record-mode child amount is refused (record-currency-aggregate-door.ts, RecordCurrencySummaryRefusedError). So on a EUR invoice the lines and the Subtotal display in the home currency's symbol, while amount shows EUR and amount_home the home currency. That is a display lie the example cannot fix from metadata. I will report it as an out-of-scope finding (class a, a user-visible misleading display) for the spec seat. It is not fixed here.

    2. Seed plan, and the blocker

    • Org currency: USD. Foreign currencies EUR and JPY: EUR to show 2 decimals, JPY 0. Rates in the ruled direction, "1 home unit = rate units": EUR 0.92 effective 90 days ago and 0.9 effective 20 days ago; JPY 151.5 and 149.25 on the same dates. That gives a dated pair per currency, so invoices on either side of the change convert at different rates. Rows go into sys_exchange_rate with source: 'manual'.
    • Invoices: all 12 seeded invoices get currency_code and an invoice_date equal to their issued_on (or today on drafts). INV-1004, INV-1009 and INV-1010 (EMEA) become EUR, and INV-1007 / INV-1008 (APAC) become JPY. The rest stay USD. Today only INV-1001 to INV-1004 have lines, so INV-1008 (JPY) gets two yen-priced lines, so that one foreign invoice in each foreign currency shows a non-zero conversion.
    • ⚠ Blocker: there is no app or seed door for localization.currency. The value comes from three places today: the --seed-admin dev seed derives it from the host's POSIX locale (plugin-auth seedDevWorkspaceCurrency), organization creation derives it from the creator's locale, and an env rung (OS_LOCALIZATION_CURRENCY, envKeyOf) can set it. Measured in this container: LANG, LC_ALL and LC_MONETARY are all unset, so the dev seed writes nothing. Then every seeded invoice carrying a code and a non-blank amount is refused ERR_HOME_CURRENCY_UNSET. The seeder drops refused rows with a warning, so all 12 invoices would vanish, and the Revenue Pulse, invoice RLS and approval demos would lose their data. On a host whose locale gives another home currency (CNY), the USD-relative rates would convert silently wrong. The seed therefore needs a fixed home currency. Three ways to get one:
      • A (recommended): add OS_LOCALIZATION_CURRENCY=USD to examples/app-showcase/.env, beside OS_PAGE_REACT_ENABLED. That file is committed and read by os dev / os serve / os start, and the projection reads the currency through the settings service, env rung included (objectql plugin.ts setHomeLocalizationResolver). Cost: the Localization page shows the currency pinned by env. Gap: in-process harnesses (the dogfood suites that boot the showcase, the showcase vitest, e2e) do not read .env. If any of them asserts on seeded invoices, it needs the same env, and that is outside examples/**. I will list which ones before touching any.
      • B: a showcase kernel:bootstrapped bootstrap (precedent src/security/seed-approval-demo.ts) that calls the settings service's set when the currency is unset, then writes the rates and invoices itself. The invoices then leave the static seed, and the bootstrap must skip with a log line when the host already set another currency.
      • C: a platform door, per-organization settings defaults declared by an app. That is a new stack key and platform code, so it is out of this fence and would need its own card.
    • Which option? A keeps the seed declarative and the boot deterministic. B moves data into imperative code. C is a capability with one consumer.

    3. Flow template: showcase_exchange_rate_sync

    • type: 'schedule', start schedule: { type: 'cron', expression: '0 6 * * *' }. It ships status: 'draft', so it is a template the customer turns on, and it makes no external call on any boot or in CI. Scheduled package work is also off by default (OS_AUTOMATION_SCHEDULED_WORK_ENABLED).
    • Variable rate_feed_url (text, defaultValue a public JSON rates endpoint keyed on USD). The http node's url is the text slot that reads this variable. "A setting the customer changes": no metadata door reads a settings value from a flow (no app-declared settings namespace, and no settings read in the formula stdlib). So the customer-changeable setting is this variable's default, which is edited in Studio. I say so in the file. A custom showcase_rate_feed config object read with get_record would also work, but it adds an object, so I did not pick it.
    • Nodes: start → http (GET) → assignment (a CEL map over the response's rates into a list of {currency, rate}) → loop → create_record on sys_exchange_rate with source: 'app' and effective_date from the response date → end. A same-day re-run is refused by the (currency, effective_date) unique key, so the create sits inside try_catch and a duplicate day is skipped.
    • Node types on main: start (schedule), http, assignment, loop and create_record are in FlowNodeAction (flow.zod.ts), and try_catch is a builtin region (ADR-0031, service-automation/src/builtin/try-catch-node.ts). All have builtin executors. No platform change.

    4. Checklist item

    • Where: a new item in docs/qa/platform-checklist/areas/records-forms.json, which already owns the currency field clauses, plus its run record under docs/qa/platform-checklist/runs/, following .claude/skills/checklist-test/SKILL.md.
    • Text (draft): "Multi-currency invoice: with home currency USD and seeded EUR/JPY rates, a EUR invoice stores amount_home = amount ÷ the rate in force on invoice_date, and amount_rate = that rate. The home field renders read-only with its 'Converted at RATE on DATE' hint, and the form shows the live '≈ home amount' preview. A JPY invoice dated before any JPY rate is refused ERR_EXCHANGE_RATE_MISSING, naming the currency and date. Changing currency_code re-projects the amount."
    • Verify steps: a REST create/read for each clause, and a browser pass for the form face. Screenshots at deviceScaleFactor 2 go in the PR body.

    5. Fence

    • Inside examples/app-showcase/** (invoice object, seed, flow, .env if A, showcase translations if any are generated) and docs/qa/platform-checklist/**. No platform package code.
    • Out of the fence: only §2's harness gap under A, and only if an in-process harness asserts on seeded invoices. I will list them and ask before touching one.
    • The Revenue Pulse dataset sums the invoice total. Once invoices mix currencies, that sum mixes them too. Pointing it at amount_home is in examples/**, but it is not in the card's task list. I propose to include it and will leave it out if the seat says no.
    • @objectstack/example-showcase has "private": true, so the label is skip-changeset. Clause-②: no · Metadata-protocol: none, as the Claim says.

    Questions for the seat: (1) A, B or C for the home currency; (2) the Revenue Pulse measure, in or out.

  8. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6

    Seat ruling on step 1 (6730090645) · 2026-11-09T01:31Z

    1. Home currency at seed time: A. Add OS_LOCALIZATION_CURRENCY=USD to the committed examples/app-showcase/.env, so the seeds stay declarative (the showcase is the template AI authors copy). B, an imperative bootstrap seed, would teach the wrong path. C, a settings-defaults door, would be a new platform card. Before code, the dev lists every in-process harness that seeds the showcase and asserts on invoices or anything built from them. A claim amendment comes only if one must set the same env in its test setup. The PR body states that a host with no .env reader and no currency seeds zero invoices.
    2. Revenue Pulse sums amount_home: in. It is in examples/**, and a sum across mixed currencies would be wrong.
    3. Out-of-scope finding: line items and the Subtotal roll-up cannot follow the header's record currency. A child cannot point currencyField at its parent, and a summary over a record-mode child is refused. The seat files this as its own spec card. It is not in this PR.
  9. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · zhuangjianguo · session_01QsR2jTo666XXx9e1Gm21C6

    Claim: PM loop round 8 · amendment · 2026-11-09T01:33Z
    Session: session_01QsR2jS3Cr7JjyVM85apubn (card thread, dev) · seat session_01QsR2jTo666XXx9e1Gm21C6
    Account: zhuangjianguo
    Branch: claude/project-thread-jdqqde
    Worktree: objectstack-issue-27197
    Domain: domain:devx
    Seat: domain:devx#2
    File surface: examples/app-showcase/** (the invoice object, seeds, the exchange-rate flow template, Revenue Pulse on amount_home, .env with OS_LOCALIZATION_CURRENCY=USD, and generated translations if any), docs/qa/platform-checklist/** and its run record, plus packages/qa/dogfood/test/showcase-boot.ts (test setup only: set OS_LOCALIZATION_CURRENCY=USD before boot, because the in-process boot reads no .env). Any further harness file is named to the seat before it is touched. ⛔ No platform package code, no packages/spec/**, no ui/.github/**.
    Container & model: M, mode:thread, model: default tier (no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the showcase's multi-currency example | invoice has no currency code, home amount or document date, though #27192 and #27196 shipped them | AI authors who copy the showcase, and the platform checklist
    Thread-read: 6729749606
    Premise: holds; both Blocked-by: cards are closed (6729749606), and the step-1 comment re-reads the merged declarations
    Serial constraints cleared: none open. No other devx PR touches examples/app-showcase/**.

    Scope: step 1 (declarations, seeds, flow shape, checklist item) is posted before code. Then one PR. The PR says Fixes #27197. Brief: /mnt/project-files/issue-27197/brief.md. Summons: ruling 6465431966 (card body), unblocked by 6729749606.

  10. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · zhuangjianguo · session_01QsR2jS3Cr7JjyVM85apubn
    os-dev-report

    {
      "card": 27197,
      "pr": 31757,
      "branch": "claude/project-thread-jdqqde",
      "head": "7fcfbff0282ae8c3d81673cc03c1fd099f126702",
      "base_merged": "f65c36fcca8c33d38c62104c0888c950a315089a",
      "draft": true,
      "labels": ["skip-changeset"],
      "declarations": { "clause_2": "no", "metadata_protocol": "none" },
      "gates": {
        "derived": 56, "run": 55, "not_measured": 1, "unrun": 0,
        "not_measured_list": ["pnpm check:dual-build-cjs-loads (exit 3, unbuilt packages outside this diff)"],
        "nonzero_after_fix": [],
        "note": "check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 after fetching its pinned fixture sha; sweep ran on 6d2c5b3a (pre-merge, same PR files)",
        "body_gates": { "check-changeset-no-major": 0, "check-adr-0087-registration": 0, "check-closing-keyword-parity": 0, "check-partof-closing-keyword": 0 }
      },
      "tests": {
        "showcase_typecheck": 0, "showcase_validate": 0,
        "showcase_vitest": "46 files / 544 tests passed (re-run after merge of main)",
        "dogfood_showcase_booters": "248 files: 246 passed, 2 skipped; 1832 tests passed, 11 skipped",
        "dogfood_invoice_writers_rerun": "17 files / 125 tests passed after the record-page and rounding commit",
        "check_platform_checklist": 0, "check_i18n_coverage": 0
      },
      "live_verification": {
        "item": "records-forms.multi-currency-invoice rev 1",
        "clauses": { "pass": 6, "partial": 0, "fail": 0, "blocked": 0 },
        "run_record": "none: verification run caused by this PR's checklist edit (checklist-test skill section 5); clause table in the PR body",
        "screenshots": "8 at deviceScaleFactor 2, project files issue-27197/shots; API readings issue-27197/api-probe.json"
      },
      "line_budget": { "files": 12, "insertions": 441, "deletions": 17, "note": "against origin/main at merge, before the one-line history ref commit" },
      "files_changed": [
        "docs/qa/platform-checklist/areas/records-forms.json",
        "examples/app-showcase/.env",
        "examples/app-showcase/src/automation/flows/exchange-rate-sync.flow.ts",
        "examples/app-showcase/src/automation/flows/index.ts",
        "examples/app-showcase/src/data/objects/invoice.object.ts",
        "examples/app-showcase/src/data/seed/index.ts",
        "examples/app-showcase/src/system/translations/index.ts",
        "examples/app-showcase/src/ui/dashboards/revenue-pulse.dashboard.ts",
        "examples/app-showcase/src/ui/datasets/revenue-pulse.dataset.ts",
        "examples/app-showcase/src/ui/pages/invoice-document.page.ts",
        "packages/qa/dogfood/test/showcase-boot.ts"
      ],
      "deviations": [
        "invoice-document.page.ts (inside examples/app-showcase/**, not named in step 1): the invoice record page lists its sections explicitly, so the new fields were invisible until added there",
        "amount is labelled 'Total incl. Tax' (step 1: 'Invoice Total'), because the bundle labels total 'Total'; its expression rounds to 2 decimals (measured 1749.5568 stored before)",
        "currency_code is optional, not required as step 1 drafted: seat ruling A2",
        "the flow ships status 'obsolete', not 'draft' as step 1 said: a draft flow binds and fires like an active one; obsolete is loaded, not armed",
        "rates are dated 2025-01-01 and 30 days ago (step 1: 90 and 20 days ago), so seed invoices dated across the year fall on both rows",
        "builds, the dogfood battery, the console build and the gate sweep ran under run_in_background (common rule 9 asks foreground): each exceeds the 10-minute foreground cap"
      ],
      "out_of_scope_findings": [
        {
          "class": "a",
          "reach": "browser invoice form and record page, any record-mode amount with child lines",
          "finding": "invoice lines and the Subtotal roll-up cannot follow the header's record currency (a child currencyField cannot name the parent's code field; a summary over a record-mode child amount is refused, RecordCurrencySummaryRefusedError), so a EUR or JPY invoice shows its line prices and Subtotal with the home '$'",
          "evidence": "detail-inv1008-jpy.png and edit-form-jpy-2024.png"
        },
        {
          "class": "a",
          "reach": "browser edit form and REST, any currency field with a stored expression",
          "finding": "the auto-layout form draws a stored-expression currency field as an editable input; a typed value is accepted and silently replaced by the expression on save",
          "evidence": "PATCH /api/v1/data/showcase_invoice/ID with amount 999 answered 200; read-back amount 323.94 (the expression's value); edit-form-eur.png shows the input"
        }
      ],
      "security_findings": "none",
      "api_writes": 3,
      "api_writes_list": ["step-1 comment on #27197 (post-stamped)", "pr_create #31757 (relay)", "this report (post-stamped)"],
      "mcp_calls": { "writes": 0, "reads": "issue_read (card and comments, before a context reset; count not retained), pull_request_read 1 (#31724 body shape)", "subscribe_pr_activity": 1 }
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:devxDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions