Repository navigation
Commit a599cc1
test(qa-scenarios): add the order_to_cash mid-tier scenario card (#30706)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D92W26tBPQgYaQxn6AjhUbM)_
Fixes #30477
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:新增「订单到收款」中档场景卡(8
个对象:员工、客户、产品、销售订单、订单明细、发票、收款、跟进活动;折扣审批加逾期催收自动化;3 个角色其中 2 个按行限定;1
张应收账龄报表;120 张订单跨 7–10 月;10 道问数、6 个写操作、5 个 agent 动作),并配一份只属于这张卡的真值测试。
- **为什么改**:落实 cloud#3600 九个场景中的订单到收款场景,让 AI
建应用、问数、代办写操作都有一张能用代码判分的销售到财务闭环卡。
- **风险与回滚**:只加测试数据和测试,所在包私有、不发布,运行时零改动,也不碰任何共享文件;回滚即删两个新文件。
- **先看**:① `docs/qa/scenarios/order_to_cash.json` 的 `run.questions` 与
`run.operations`;② `packages/qa/scenarios/test/order-to-cash.test.ts` 的
`EXPECTED_TRUTHS` 与 `EXPECTED_PERSONA_TRUTHS`;③
同文件的种子一致性用例(发票、收款、账龄与催收规则)。
- **两分钟验证**:`pnpm --filter @objectstack/scenarios test`,预期 5 个测试文件、70
个用例全绿。
- **需要你做的**:无,席位落地。
## What changes
- `docs/qa/scenarios/order_to_cash.json` (new): the 订单到收款 order-to-cash
mid-tier card.
- Eight objects: `employee`, `account`, `product`, `sales_order`,
`order_line`, `invoice`, `payment` and `activity` (the account's
follow-up stream, its own records with lookups to the account, the
invoice and the owner, never typed into the account).
- The flows: an order with a line discounted above 10% is routed to the
owner's sales manager (`approver`, `approved_on`), and an invoice 15
days past due and not fully paid gets a collection call for the AR
clerk.
- Three personas: `sales_rep` (e03, own accounts; no payment object, no
product cost) and `sales_manager` (e01, team east; no payment bank
reference) carry a row scope; `ar_clerk` (e09) sees everything but
product cost.
- Seed: 120 orders dated July to October 2026, plus 240 lines, 95
invoices, 72 payments and 64 activities. The arithmetic rule that
generated it is stated in the card's `$comment`, so every truth can be
worked by hand. Receivables are as of `asOf` 2026-10-31: one account has
paid nothing, one pays half on the due date, one pays 20 days late.
- Run half: 10 questions, 6 operations and 5 agent actions (one
confirm-gated); `iterate` adds credit notes.
- `packages/qa/scenarios/test/order-to-cash.test.ts` (new): this card's
own test file.
- It pins all 10 computed truths and the 8 per-persona truths against
hand-worked values, with the derivations written beside the pins.
- It asserts the mid-tier floor, and checks the seed against its own
rule: line amounts, order totals, approval routing, one invoice per
fulfilled order, the payment roll-up, balance, status and aging bucket,
and the collection calls.
- It checks that each write's declared outcome agrees with the seed (the
targeted rows' state, the totals, the open balance the overpayment
exceeds).
- It shows that a wrong seed cell and a wrong truth spec in a fixture
copy go red against those pins.
## Tests
`pnpm --filter @objectstack/scenarios test`: 5 files, 70 tests passed
(64 before this card, 6 new). `pnpm --filter @objectstack/scenarios
typecheck`: exit 0, after building the closure with `pnpm --filter
'@objectstack/scenarios^...' build`.
The pinned values were worked from the seed rule by a script that
implements the business rule directly (it never reads the card's truth
specs or calls the loader), then matched against the loader's computed
truths: every pin agreed on the first run. The ranking, aging,
subscription and per-persona derivations are written out by hand beside
their pins.
## Acceptance notes
- **File name.** The card is `order_to_cash.json`, not
`order-to-cash.json` as the issue spells it. A card's `id` must be a
snake_case machine name and equal the file's basename, so a hyphenated
basename cannot load. The test file follows the merged sibling's
spelling, `order-to-cash.test.ts`.
- **No shared file touched.** The travel-expense card's PR made
`test/library.test.ts` assert only the three control cards and moved
per-card pins to each card's own file, so this PR edits neither
`library.test.ts` nor the README. Sibling cards no longer conflict with
it.
- **README spelling.** `docs/qa/scenarios/README.md` "Adding a card"
step 3 names the pin file `test/ID.test.ts`, while its model and this
card use a hyphenated basename. An observation only; the README is
unchanged here.
- **No schema field for some floor items.** The approval flow, the
automation, "declarative preferred" and a report as distinct from a
dashboard have no field in the schema. The card states them in
`prompt.detailed` (zh and en) and in `capabilities`, seeds the approval
on `sales_order` (`approver`, `approved_on`) and the automation's
collection calls as `activity` rows, and declares the report under
`expect.dashboards`. Nothing in the loader grades them.
- **Seven operation kinds, six operations.** `order_from_text` covers
two of them: entry from free text with lines, and the multi-step chain
(create the order, add two lines, submit it, route it to the manager).
The workflow step is `approve_discount`; its progress query is the
question `so_0120_waiting_on`.
- **Two writes on SO-0110.** `rep_approves_own_discount` (refused) is
listed before `approve_discount` (applied). A refused write changes
nothing, so both outcomes hold whether a runner grades each write
against the seed or applies the writes in order.
- **Created rows matched by `where`.** A `where` cannot name the new
order's id, so `order_from_text` locates the order by account and
today's date and each line by product, quantity and discount; no seeded
row matches any of them. The lines are therefore not tied to the new
order in the post-state.
- **Stored aging bucket.** `invoice.aging_bucket` is a formula on today
in the built app; the seed stores its value as of `asOf`, and the test
holds every bucket to the due date.
- **Capability tags.** `approvals.dynamic-approver-routing` and
`automation.time-relative-trigger` are checklist items; the other tags
follow the existing cards' wording. The loader checks the area half
only.
- **Changeset.** `@objectstack/scenarios` is `private: true` and nothing
else moves, so this PR carries `skip-changeset`, as the travel-expense
card's PR did.
- **Unreachable references.** The domain is designed from the card text
and general business knowledge alone; objectstack-ai/cloud#3567 was not
read.
## Verification
<details>
<summary>scenarios test + typecheck (head e44efca)</summary>
```
Test Files 5 passed (5)
Tests 70 passed (70)
> tsc --noEmit
os-verify-lock: VERDICT command-exit 0
```
</details>
<details>
<summary>dispatch-gates --commands (50 commands, local, head
e44efca)</summary>
All 50 derived commands exit 0 on this head (the merge of `main` at
e44efca), and on the pre-merge head 5d751a9 before it. The `--ran`
reconciliation reads: 50 derived, 50 run, 0 NOT-MEASURED, 0 UNRUN, a
derived zero because every line recorded its exit code.
On the first run two of them exited 3 (PREREQUISITE NOT MET) and exited
0 once the prerequisite was met:
- `check-plugin-teardown-shape.mjs --self-test`, after fetching its
pinned positive-control commit into the shallow clone;
- `check:dual-build-cjs-loads`, after a full `turbo run build` (119 of
119 tasks).
After the merge of `main` (which moved `packages/spec`): `pnpm --filter
@objectstack/spec build` then `check:generated` reported all 14
generated artifacts up to date.
Changed lines: 1379 (+1379 / -0, 2 files), under the 3000 threshold.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4d1Exmm9uiWTdqYBhqM)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent aed9663 commit a599cc1
2 files changed
Lines changed: 1379 additions & 0 deletions
0 commit comments