feat: multi-currency support and E2E tests for Kotlin sample#306
feat: multi-currency support and E2E tests for Kotlin sample#306
Conversation
- 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis 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:
Issues found:
Confidence Score: 4/5Not 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.
|
| 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
Comments Outside Diff (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
beneficiaryNodeis read fromaccountInfo.get("beneficiary"), which traverses into theaccountInfosub-object. But both the frontend (CreateExternalAccount.tsx) and the E2E tests sendbeneficiaryas a top-level field in the request body, not nested insideaccountInfo:{ "accountInfo": { "accountType": "MXN_ACCOUNT", ... }, "beneficiary": { "fullName": "Carlos Test", ... } // ← top level }Because
accountInfonever contains abeneficiarykey,beneficiaryNodeis alwaysnull.parseBeneficiaryFields(null)then returnsBeneficiaryFields("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
jsonbeneficiary node intobuildAccountInfo:// 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>
Summary
optText/requireTexttoJsonUtils.kt, deduplicateBeneficiaryFieldsparsingTest plan
cd samples/kotlin && ./gradlew test— 7 backend E2E tests passcd samples/frontend && npm run test:e2e— 8 Playwright browser tests pass (requires backend running)🤖 Generated with Claude Code