Skip to content

feat: multi-currency support and E2E tests for Kotlin sample#306

Open
pengying wants to merge 4 commits intomainfrom
feat/sample-multi-currency-e2e-tests
Open

feat: multi-currency support and E2E tests for Kotlin sample#306
pengying wants to merge 4 commits intomainfrom
feat/sample-multi-currency-e2e-tests

Conversation

@pengying
Copy link
Copy Markdown
Contributor

Summary

  • Fix SDK type mismatches so the Kotlin sample compiles and runs against SDK v1.1.0
  • Add multi-currency external account support (MXN, BRL, INR, GBP, PHP, EUR) with country selector dropdown
  • Add source currency dropdown (USD/USDC) on the quote step
  • Add "Start New Payment" button to restart the flow from the external account step
  • Extract shared optText/requireText to JsonUtils.kt, deduplicate BeneficiaryFields parsing
  • Add 7 Ktor server E2E tests hitting the Grid sandbox API
  • Add 8 Playwright browser E2E tests driving the full React UI

Test plan

  • cd samples/kotlin && ./gradlew test — 7 backend E2E tests pass
  • cd samples/frontend && npm run test:e2e — 8 Playwright browser tests pass (requires backend running)
  • Manual testing of all country destinations (IN, MX, BR, PH, GB, EU)
  • Manual testing of USD and USDC source currencies
  • Manual testing of "Start New Payment" reset flow

🤖 Generated with Claude Code

- Fix SDK type mismatches (Config field names, QuoteSourceOneOf, AccountType enums)
- Add multi-currency external account support (MXN, BRL, INR, GBP, PHP, EUR)
- Add country selector dropdown and source currency dropdown (USD/USDC)
- Add "Start New Payment" button to restart from external account step
- Extract shared optText/requireText to JsonUtils, deduplicate across route files
- Extract BeneficiaryFields to reduce copy-paste across beneficiary builders
- Add Ktor server E2E tests (7 tests hitting Grid sandbox API)
- Add Playwright browser E2E tests (8 tests driving the full React UI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grid-flow-builder Ready Ready Preview, Comment Mar 27, 2026 10:54pm

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR adds multi-currency external account support (MXN, BRL, INR, GBP, PHP, EUR) to the Kotlin sample, updates the frontend with country and source-currency selectors, adds a "Start New Payment" reset flow, and adds 7 Ktor backend E2E tests plus 8 Playwright browser E2E tests. The overall direction is solid, but there is one P1 data-correctness bug in the backend.

Key changes:

  • ExternalAccounts.kt: New buildAccountInfo dispatch function with per-currency builder helpers and a shared parseBeneficiaryFields extractor
  • CreateExternalAccount.tsx / CreateQuote.tsx: Country and source-currency dropdowns wired to App-level state
  • JsonUtils.kt: Shared optText/requireText extension functions extracted from per-file duplicates
  • EndToEndTest.kt + payout-flow.spec.ts: End-to-end coverage for the full payout flow

Issues found:

  • P1 — ExternalAccounts.kt: buildAccountInfo reads beneficiaryNode from accountInfoNode.get(\"beneficiary\") instead of the top-level request body. Every external account is silently created with \"Account Holder\" as the beneficiary name.
  • P2 — CreateQuote.tsx: selectedCountry is listed in the useEffect dependency array but never read inside the effect.
  • P2 — Quotes.kt: json.get(\"lockedCurrencyAmount\").asLong() is not null-guarded; a missing field produces a 500 instead of a 400.

Confidence Score: 4/5

Not safe to merge as-is — the P1 beneficiary bug means all external accounts are created with a placeholder name regardless of submitted data.

One confirmed P1 data-correctness defect: beneficiary information sent by every caller is silently discarded and replaced with Account Holder. The fix is a one-line change but must land before merge.

samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExternalAccounts.kt — beneficiary node read from the wrong JSON path at line 99.

Important Files Changed

Filename Overview
samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExternalAccounts.kt Major refactor adding 6 new currency types; P1 bug: beneficiaryNode is read from accountInfoNode instead of the top-level request body, silently discarding all beneficiary data.
samples/frontend/src/steps/CreateExternalAccount.tsx Adds country selector dropdown with 6 currency configs; platformAccountId regenerates on every country switch (previously flagged); otherwise clean UI wiring.
samples/frontend/src/steps/CreateQuote.tsx Adds source currency dropdown (USD/USDC); selectedCountry in useEffect deps is unused inside the effect, causing unnecessary body resets on country change.
samples/frontend/src/App.tsx Adds selectedCountry state, country change callback, and Start New Payment reset button; clean and straightforward.
samples/kotlin/src/test/kotlin/com/grid/sample/EndToEndTest.kt 7 Ktor E2E tests covering the main payout flow; tests pass but don't assert beneficiary fields, hiding the P1 beneficiary node bug.
samples/frontend/e2e/payout-flow.spec.ts 8 Playwright E2E tests covering the full UI flow including country switching, source currency dropdown, and reset; well structured.
samples/kotlin/src/main/kotlin/com/grid/sample/JsonUtils.kt New file extracting shared optText/requireText JSON helpers and JsonUtils object; clean utility extraction.
samples/kotlin/src/main/kotlin/com/grid/sample/routes/Quotes.kt Adds multi-currency source support; unguarded get(lockedCurrencyAmount) can NPE into a 500 when field is missing.

Sequence Diagram

sequenceDiagram
    participant UI as React Frontend
    participant BE as Kotlin Backend
    participant Grid as Grid Sandbox API

    UI->>BE: POST /api/customers
    BE->>Grid: customers.create(params)
    Grid-->>BE: Customer { id }
    BE-->>UI: 201 { id }

    UI->>BE: POST /api/customers/{id}/external-accounts
    Note over BE: buildAccountInfo reads beneficiaryNode
    Note over BE: from accountInfoNode (wrong!) always null
    BE->>Grid: externalAccounts.create(params)
    Grid-->>BE: ExternalAccount { id }
    BE-->>UI: 201 { id }

    UI->>BE: POST /api/quotes
    BE->>Grid: quotes.create(params)
    Grid-->>BE: Quote { id }
    BE-->>UI: 201 { id }

    UI->>BE: POST /api/sandbox/send-funds
    BE->>Grid: sandbox.sendFunds(params)
    Grid-->>BE: FundingResult
    BE-->>UI: 200 OK
Loading

Comments Outside Diff (1)

  1. samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExternalAccounts.kt, line 98-100 (link)

    Beneficiary node read from wrong JSON path — data silently discarded

    beneficiaryNode is read from accountInfo.get("beneficiary"), which traverses into the accountInfo sub-object. But both the frontend (CreateExternalAccount.tsx) and the E2E tests send beneficiary as a top-level field in the request body, not nested inside accountInfo:

    {
      "accountInfo": { "accountType": "MXN_ACCOUNT", ... },
      "beneficiary": { "fullName": "Carlos Test", ... }   // ← top level
    }

    Because accountInfo never contains a beneficiary key, beneficiaryNode is always null. parseBeneficiaryFields(null) then returns BeneficiaryFields("Account Holder", null, null, null), meaning every external account is created with a generic placeholder beneficiary name regardless of what the caller sent. The E2E tests don't assert the beneficiary fields in the response, so they pass without exposing this.

    The fix is to pass the top-level json beneficiary node into buildAccountInfo:

    // In the route handler, replace:
    val accountInfo = buildAccountInfo(accountType, accountInfoNode)
    
    // With:
    val accountInfo = buildAccountInfo(accountType, accountInfoNode, json.get("beneficiary"))
    // Update the function signature:
    private fun buildAccountInfo(
        accountType: String,
        accountInfo: JsonNode,
        beneficiaryNode: JsonNode? = null
    ): ExternalAccountInfoOneOf {
        // Remove: val beneficiaryNode = accountInfo.get("beneficiary")
Prompt To Fix All With AI
This is a comment left during a code review.
Path: samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExternalAccounts.kt
Line: 98-100

Comment:
**Beneficiary node read from wrong JSON path — data silently discarded**

`beneficiaryNode` is read from `accountInfo.get("beneficiary")`, which traverses into the `accountInfo` sub-object. But both the frontend (`CreateExternalAccount.tsx`) and the E2E tests send `beneficiary` as a **top-level** field in the request body, not nested inside `accountInfo`:

```json
{
  "accountInfo": { "accountType": "MXN_ACCOUNT", ... },
  "beneficiary": { "fullName": "Carlos Test", ... }   // ← top level
}
```

Because `accountInfo` never contains a `beneficiary` key, `beneficiaryNode` is always `null`. `parseBeneficiaryFields(null)` then returns `BeneficiaryFields("Account Holder", null, null, null)`, meaning every external account is created with a generic placeholder beneficiary name regardless of what the caller sent. The E2E tests don't assert the beneficiary fields in the response, so they pass without exposing this.

The fix is to pass the top-level `json` beneficiary node into `buildAccountInfo`:

```kotlin
// In the route handler, replace:
val accountInfo = buildAccountInfo(accountType, accountInfoNode)

// With:
val accountInfo = buildAccountInfo(accountType, accountInfoNode, json.get("beneficiary"))
```

```kotlin
// Update the function signature:
private fun buildAccountInfo(
    accountType: String,
    accountInfo: JsonNode,
    beneficiaryNode: JsonNode? = null
): ExternalAccountInfoOneOf {
    // Remove: val beneficiaryNode = accountInfo.get("beneficiary")
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: samples/frontend/src/steps/CreateQuote.tsx
Line: 26-42

Comment:
**`selectedCountry` in `useEffect` deps is unused inside the effect**

`selectedCountry` appears in the dependency array (line 41) but the effect body never reads it — neither directly nor through `destCurrency`, which is computed outside the effect and not referenced inside it. As a result the entire quote body is reset to defaults every time the user switches destination country on the previous step, which could silently overwrite any manual edits the user made to the textarea.

If the intent is to expose `destCurrency` in the JSON (e.g. for informational purposes), consider adding it to the body. If the country change shouldn't reset the body, remove `selectedCountry` from the deps:

```suggestion
  }, [customerId, externalAccountId, sourceCurrency])
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: samples/kotlin/src/main/kotlin/com/grid/sample/routes/Quotes.kt
Line: 37

Comment:
**Unchecked `get("lockedCurrencyAmount")` throws NPE when field is absent**

`json.get("lockedCurrencyAmount").asLong()` will throw a `NullPointerException` if the client omits the field. This is caught by the outer `catch (e: Exception)` block and returned as a 500 instead of a 400, which makes debugging harder for API consumers.

Consider using a safe accessor with a fallback or an explicit null check:

```suggestion
                    .lockedCurrencyAmount(json.get("lockedCurrencyAmount")?.asLong() ?: 1000L)
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (4): Last reviewed commit: "fix: use countryOfResidence instead of n..." | Re-trigger Greptile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- India: name, UPI, countryOfResidence
- Mexico: name, CLABE, countryOfResidence
- Brazil: name, taxId, pixKeyType, pixKey, countryOfResidence
- Philippines: name, bankCode, accountNumber, countryOfResidence
- EU: name, IBAN, full address, countryOfResidence
- UK: name, sortCode, accountNumber, countryOfResidence

Removes extra fields (birthDate, email, phone, swiftCode) that are not
required per the spec. Makes swiftCode optional in the EUR backend handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API field is countryOfResidence, not nationality. Updated frontend
configs, backend parser (with fallback to nationality), and E2E tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant