Sync Grid Visualizer with PaymentRail enum - #351
Conversation
Update currencies.ts to use SCREAMING_SNAKE_CASE payment rail values that match the PaymentRail enum in the OpenAPI spec (e.g., FEDNOW, FASTER_PAYMENTS, SEPA_INSTANT, BANK_TRANSFER, MOBILE_MONEY). Add paymentRails field to generated external account creation requests in code-generator.ts to match documented workflow examples. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR syncs the Grid Visualizer with the new Confidence Score: 4/5The currencies.ts enum update is safe, but code-generator.ts injects a response-only field into create request bodies which would produce incorrect example code. One P1 finding: paymentRails is injected into accountInfo for external account creation requests, but the field is absent from all create-request schemas (*AccountInfoBase). This generates misleading or broken example API calls, which is the core purpose of this visualizer. The P2 (enum values in prose) is minor. Score is 4 until the P1 is resolved. components/grid-visualizer/src/lib/code-generator.ts — paymentRails field placement in buildAccountInfoBody
|
| Filename | Overview |
|---|---|
| components/grid-visualizer/src/data/currencies.ts | Rail values updated to SCREAMING_SNAKE_CASE matching the PaymentRail enum; all new values (FEDNOW, WIRE, SEPA_INSTANT, FASTER_PAYMENTS, BANK_TRANSFER, MOBILE_MONEY, PAYNOW) are valid enum members. |
| components/grid-visualizer/src/lib/code-generator.ts | Adds paymentRails to accountInfo in external account create requests, but this field exists only in response schemas (*AccountInfo), not create request schemas (*AccountInfoBase) — the generated example code would send an undocumented field to the API. |
Sequence Diagram
sequenceDiagram
participant UI as Grid Visualizer UI
participant CG as code-generator.ts
participant CUR as currencies.ts (PaymentRail enum)
participant API as Grid API
UI->>CG: generateSteps(source, destination)
CG->>CUR: find(c => c.code === sel.code)
CUR-->>CG: fiat.allRails (SCREAMING_SNAKE_CASE)
CG->>CG: buildAccountInfoBody() sets info.paymentRails = allRails
CG-->>UI: ApiStep with accountInfo.paymentRails
UI->>API: POST /customers/external-accounts { accountInfo: { ..., paymentRails: [...] } }
Note over API: paymentRails not in create schema, only in response schema
Comments Outside Diff (1)
-
components/grid-visualizer/src/lib/code-generator.ts, line 309 (link)Raw enum values in user-facing note text
getJitPaymentMethod()now returns strings like"FEDNOW transfer"or"SEPA_INSTANT transfer"fromfiat.instantRails[0], which appear directly in the step note shown to the user: "Trigger the payment by sending a SEPA_INSTANT transfer to the deposit address...". The previous human-readable names (FedNow,SEPA Instant) were better suited for this prose context.Consider either keeping a separate display-name map for prose use or adding a formatting helper that converts
SEPA_INSTANT→SEPA Instantfor the note text.Prompt To Fix With AI
This is a comment left during a code review. Path: components/grid-visualizer/src/lib/code-generator.ts Line: 309 Comment: **Raw enum values in user-facing note text** `getJitPaymentMethod()` now returns strings like `"FEDNOW transfer"` or `"SEPA_INSTANT transfer"` from `fiat.instantRails[0]`, which appear directly in the step note shown to the user: *"Trigger the payment by sending a SEPA_INSTANT transfer to the deposit address..."*. The previous human-readable names (`FedNow`, `SEPA Instant`) were better suited for this prose context. Consider either keeping a separate display-name map for prose use or adding a formatting helper that converts `SEPA_INSTANT` → `SEPA Instant` for the note text. How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix All With AI
This is a comment left during a code review.
Path: components/grid-visualizer/src/lib/code-generator.ts
Line: 51-57
Comment:
**`paymentRails` is a response-only field**
`paymentRails` is not part of the external account creation request schema. Every `*ExternalAccountCreateInfo` schema (e.g. `UsdExternalAccountCreateInfo`) extends `*AccountInfoBase`, and none of those base schemas include `paymentRails`. The field only appears in the response-side `*AccountInfo` schemas (e.g. `UsdAccountInfo`) that the API returns after creation.
Sending `paymentRails` inside `accountInfo` will either cause a 400 validation error or be silently ignored — either way it generates misleading example code for developers using this visualizer as a reference. The OpenAPI spec examples in `customers_external_accounts.yaml` also omit it from create request bodies.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: components/grid-visualizer/src/lib/code-generator.ts
Line: 309
Comment:
**Raw enum values in user-facing note text**
`getJitPaymentMethod()` now returns strings like `"FEDNOW transfer"` or `"SEPA_INSTANT transfer"` from `fiat.instantRails[0]`, which appear directly in the step note shown to the user: *"Trigger the payment by sending a SEPA_INSTANT transfer to the deposit address..."*. The previous human-readable names (`FedNow`, `SEPA Instant`) were better suited for this prose context.
Consider either keeping a separate display-name map for prose use or adding a formatting helper that converts `SEPA_INSTANT` → `SEPA Instant` for the note text.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Sync Grid Visualizer with PaymentRail en..." | Re-trigger Greptile
| // Add paymentRails for fiat currencies | ||
| if (sel.type === 'fiat') { | ||
| const fiat = currencies.find((c) => c.code === sel.code); | ||
| if (fiat && fiat.allRails.length > 0) { | ||
| info.paymentRails = fiat.allRails; | ||
| } | ||
| } |
There was a problem hiding this comment.
paymentRails is a response-only field
paymentRails is not part of the external account creation request schema. Every *ExternalAccountCreateInfo schema (e.g. UsdExternalAccountCreateInfo) extends *AccountInfoBase, and none of those base schemas include paymentRails. The field only appears in the response-side *AccountInfo schemas (e.g. UsdAccountInfo) that the API returns after creation.
Sending paymentRails inside accountInfo will either cause a 400 validation error or be silently ignored — either way it generates misleading example code for developers using this visualizer as a reference. The OpenAPI spec examples in customers_external_accounts.yaml also omit it from create request bodies.
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-visualizer/src/lib/code-generator.ts
Line: 51-57
Comment:
**`paymentRails` is a response-only field**
`paymentRails` is not part of the external account creation request schema. Every `*ExternalAccountCreateInfo` schema (e.g. `UsdExternalAccountCreateInfo`) extends `*AccountInfoBase`, and none of those base schemas include `paymentRails`. The field only appears in the response-side `*AccountInfo` schemas (e.g. `UsdAccountInfo`) that the API returns after creation.
Sending `paymentRails` inside `accountInfo` will either cause a 400 validation error or be silently ignored — either way it generates misleading example code for developers using this visualizer as a reference. The OpenAPI spec examples in `customers_external_accounts.yaml` also omit it from create request bodies.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in e3191b4 — the paymentRails injection is removed, and code-generator.ts is now byte-identical to the base commit.
Confirmed the finding against current main before removing it. UsdExternalAccountCreateInfo and UsdAccountInfo both extend the same UsdAccountInfoBase; paymentRails is added only in the response wrapper:
# UsdAccountInfo.yaml (response)
allOf:
- $ref: ./UsdAccountInfoBase.yaml
- required: [paymentRails] # ← only here
# UsdExternalAccountCreateInfo.yaml (create request)
allOf:
- $ref: ./BaseExternalAccountInfo.yaml
- $ref: ../common/UsdAccountInfoBase.yaml
- required: [beneficiary] # ← no paymentRailsWorth noting the PR description justified this hunk by citing workflows.md. That reference no longer says so — main now states the opposite in two places, so there's nothing left to fix upstream:
.claude/skills/grid-api/SKILL.md: "Do not sendpaymentRails— Grid selects the rail and returns it on the created account.".claude/skills/grid-api/references/account-types.md: "The rail is chosen by Grid and returned on the created account (the responsepaymentRailsfield) — it is not a request input."
The currencies.ts half is unaffected and still needed: main continues to carry 'FedNow' / 'SEPA Instant' / 'Bank Transfer' while the spec has the PaymentRail enum. I checked every value in the diff against PaymentRail.yaml — ACH, WIRE, RTP, FEDNOW, SEPA, SEPA_INSTANT, FASTER_PAYMENTS, BANK_TRANSFER, MOBILE_MONEY, PAYNOW, FAST are all valid members.
Generated by Claude Code
The visualizer was injecting paymentRails into the body of
POST /customers/external-accounts. That field is response-only:
*ExternalAccountCreateInfo and *AccountInfo both extend the same
*AccountInfoBase, and paymentRails is added only in the response
wrapper. Generated example calls therefore sent a field the create
endpoint does not accept.
The skill reference now states this explicitly ("Do not send
paymentRails - Grid selects the rail and returns it on the created
account"), so the workflow examples this change was originally
justified by no longer say what it claimed.
Also add railDisplayName() for rendering rails as prose. Moving
currencies.ts to PaymentRail enum values is correct for the data, but
those values reach the UI in four places - the JIT payment note, the
currency picker rail list, the rail dropdown, and the flow-path
captions - which would otherwise read "SEPA_INSTANT transfer". The
enum value stays the data; the display name is presentation only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMnjJ8yWJsui7jLqqrDrL4
|
Also addressed the P2 from the Greptile summary (raw enum values in prose), which had no review thread to reply to. The scope was slightly wider than the original comment described.
Took the display-map approach suggested in the comment: The enum value remains the data everywhere; One thing I could not verify locally: Generated by Claude Code |
|
no longer relevant -- rail is not specified on create anymore |
## What Makes the Grid Visualizer's payment-rail selection functional. Today the rail is decorative — the user picks one from a dropdown and it only changes UI captions. This threads the choice into the generated `POST /quotes` request as `destination.paymentRail`. **Supersedes #351.** ## Why the `currencies.ts` rename is a prerequisite, not a cosmetic change `instantRails` / `allRails` currently hold human-readable strings (`'FedNow'`, `'SEPA Instant'`, `'Bank Transfer'`, …). Those are not values the API accepts, so **no rail could be sent at all** while the data held display strings. Converting them to `PaymentRail` enum members (`FEDNOW`, `SEPA_INSTANT`, `BANK_TRANSFER`, …) is what makes the feature possible. Because the enum values would otherwise render as `SEPA_INSTANT` at users, this adds an exported `railDisplayName()` covering all 23 enum members (with an underscores-to-spaces fallback) and applies it at the four sites that surface a rail: 1. `code-generator.ts` — `getJitPaymentMethod()`, the "sending a FedNow transfer" prose note 2. `CurrencyPicker.tsx` — `getRailsText()` and the `keywords` array in `buildFiatItem` (keywords keep **both** enum and display forms, so search still matches either) 3. `InputCard.tsx` — dropdown button label, dropdown menu items, static `fiatRail` row 4. `flow-path.ts` — the "Funds in via X" / "Funds out via X" captions **The enum value is the data; `railDisplayName()` is presentation only.** No display string reaches a request body or `onSelect`/state. The rename + display map is carried over from the closing PR on `docs/sync-20260418` (`e3191b4`). Two currencies added to `main` since that branch (El Salvador `SLV_ACCOUNT`, `CNY_ACCOUNT`) were converted here as well. ## `paymentRail` vs `paymentRails` — the distinction #351 got wrong | | | |---|---| | **`paymentRail`** (singular) | **Request** field, on quote destinations only (`quotes/AccountDestination.yaml`). Optional — omitted, Grid selects a default. | | **`paymentRails`** (plural) | **Response**-only. Lives in `*AccountInfo.yaml` wrappers, reporting what an account supports. Absent from `*ExternalAccountCreateInfo` / `*AccountInfoBase`. | #351 injected the plural into `POST /customers/external-accounts` bodies — a field that endpoint does not accept. This PR introduces **no** `paymentRails` anywhere (`grep -rn paymentRails src/` in the visualizer returns nothing), and `buildAccountInfoBody` has no rail logic at all. A short WHY comment at the emission site records the distinction. ## The source/destination asymmetry The spec puts `paymentRail` on the **destination**, but the visualizer's only rail selector was on the **Source** card. So this adds a destination-side selection rather than reusing the source's: - `useFlowBuilder.ts`: `destRail` state, `SET_DEST_RAIL` action, `setDestRail` callback — mirroring `sourceRail` exactly. Defaults via the existing `getDefaultRail()` on `SET_RECEIVE`, and recomputed on `SWAP` (matching how `sourceRail` already behaves there). - `page.tsx`: Destination `InputCard` now gets `rail` / `onRailChange`. `InputCard` already supported these generically, so no component changes were needed beyond the display-name calls. - Options stay constrained to the currency's `allRails` via the existing `getFiatRailOptions`, per "must be one of the rails supported by the destination account." The existing source rail is untouched and keeps its display purposes. `flow-path.ts`'s "Funds out via X" caption now uses the selected destination rail instead of always defaulting to `instantRails[0]` — same user choice, so it should reflect it. ## Judgment call worth a second opinion **`paymentRail` is emitted only for external destinations**, in addition to the "fiat and a rail selected" conditions. Funds credited to a Grid internal balance never traverse an external payment rail, so sending one there would generate an example the API would likely reject. This also matches how `flow-path.ts` already treats rails (the "Funds out via" caption only renders for `!next.isInternal`). A destination can become internal via **Swap**. Happy to drop the gate if reviewers disagree. Note the UI still shows a Rail row on an internal fiat destination card even though it no longer affects the body — consistent with how the Source card already behaves for internal sources. Hiding it felt like scope creep; flagging rather than fixing. ## Verification — please read **I could not run the real build.** `npm ci` in `components/grid-visualizer` fails because `@central-icons-react` requires a `CENTRAL_LICENSE_KEY` env var that is not available in this environment. **The Vercel preview build on this PR is the real check** — `next build` / `next lint` and all `.tsx` typechecking are unverified here. What I *was* able to verify locally: - **Strict typecheck of every dependency-free module** — `code-generator.ts`, `flow-path.ts`, `currencies.ts`, `settlement-rails.ts`, `account-types.ts`, `crypto.ts` under `strict: true` with the `@/*` path mapping: clean. The `.tsx` files could not be checked (missing `react` / `next` / icon types). - **Behavioral smoke test** of the compiled pure logic: - `paymentRail` appears in the quote body with the enum value, and changing the selection changes it (`SEPA_INSTANT` → `SEPA`) - key **omitted entirely** (not null/undefined) when no rail is selected, when the destination is crypto, and when the destination is internal - across every rail of every currency, no non-enum-looking value is ever emitted - "Funds out via" follows the selection, falls back to the default, and renders display names - all 23 `PaymentRail` members are covered by the display map, and every rail value in `currencies.ts` is a valid enum member - **Spec claims re-verified** against `openapi/components/schemas/quotes/AccountDestination.yaml`, `common/PaymentRail.yaml`, and the `*AccountInfo` / `*ExternalAccountCreateInfo` split. There is no test suite in `grid-visualizer` (no `test` script, no test files), so no tests were added or updated. Not verified: actual rendering, dropdown interaction, and that `CodePanel`'s `stepsKey` remount behaves as intended in the browser (`destRail` was added to both the memo deps and the key, so stale code should be impossible, but this is untested at runtime). ## Files changed - `src/data/currencies.ts` — enum rename + `railDisplayName()` - `src/hooks/useFlowBuilder.ts` — `destRail` state/action/setter - `src/lib/code-generator.ts` — `destRail` param, `paymentRail` emission, display names in the JIT note - `src/lib/flow-path.ts` — `destRail` param, "Funds out via" uses it, display names - `src/app/page.tsx` — wires `destRail` to the Destination card and both panels - `src/components/CodePanel/CodePanel.tsx` — `destRail` prop, memo deps, `stepsKey` - `src/components/FlowPanel/FlowPanel.tsx` — `destRail` prop, memo deps - `src/components/InputCard/InputCard.tsx` — display names - `src/components/CurrencyPicker/CurrencyPicker.tsx` — display names --- _Generated by [Claude Code](https://claude.ai/code/session_01MMnjJ8yWJsui7jLqqrDrL4)_ Co-authored-by: Claude <noreply@anthropic.com>
Summary
Syncs the Grid Visualizer with the new PaymentRail enum added in #347:
FEDNOWinstead ofFedNow,FASTER_PAYMENTSinstead ofFaster Payments,SEPA_INSTANTinstead ofSEPA Instant,BANK_TRANSFERinstead ofBank Transfer,MOBILE_MONEYinstead ofMobile Money)paymentRailsarray to generated external account creation requests, matching the documented workflow examples in.claude/skills/grid-api/references/workflows.mdTrigger
OpenAPI schema changes in commit 4d89702:
openapi/components/schemas/common/PaymentRail.yaml(new enum)openapi/components/schemas/exchange_rates/ExchangeRate.yaml(addeddestinationPaymentRail)openapi/components/schemas/quotes/AccountDestination.yaml(added optionalpaymentRail)Test plan
paymentRailsin request body🤖 Generated with Claude Code