Skip to content

Commit 75a5bca

Browse files
pengyingclaude
andcommitted
feat(samples): add Kotlin backend routes and core infrastructure
Add all Kotlin source files for the Grid API sample app backend: - Core infrastructure: Config, GridClientBuilder, WebhookStream, JsonUtils - Application entry point with CORS and SSE configuration - Route handlers: Customers, ExternalAccounts, Quotes, Sandbox, Webhooks, SSE - Fix build.gradle.kts to use correct SDK coordinates (com.grid.api:grid-kotlin) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 621bd84 commit 75a5bca

12 files changed

Lines changed: 531 additions & 1 deletion

File tree

samples/kotlin/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ application {
1919
}
2020

2121
repositories {
22+
mavenLocal()
2223
mavenCentral()
2324
}
2425

@@ -32,7 +33,7 @@ dependencies {
3233
implementation("io.ktor:ktor-server-config-yaml:3.1.3")
3334

3435
// Grid Kotlin SDK
35-
implementation("com.lightspark.grid:lightspark-grid-kotlin:0.4.0")
36+
implementation("com.grid.api:grid-kotlin:0.0.1")
3637

3738
// JSON
3839
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.grid.sample
2+
3+
import com.grid.sample.routes.*
4+
import io.ktor.http.*
5+
import io.ktor.server.application.*
6+
import io.ktor.server.plugins.cors.routing.*
7+
import io.ktor.server.routing.*
8+
import io.ktor.server.sse.*
9+
10+
fun main(args: Array<String>) {
11+
io.ktor.server.netty.EngineMain.main(args)
12+
}
13+
14+
fun Application.module() {
15+
install(CORS) {
16+
allowMethod(HttpMethod.Options)
17+
allowMethod(HttpMethod.Get)
18+
allowMethod(HttpMethod.Post)
19+
allowHeader(HttpHeaders.ContentType)
20+
allowHeader(HttpHeaders.Authorization)
21+
allowCredentials = true
22+
anyHost()
23+
}
24+
install(SSE)
25+
routing {
26+
customerRoutes()
27+
externalAccountRoutes()
28+
quoteRoutes()
29+
sandboxRoutes()
30+
webhookRoutes()
31+
sseRoutes()
32+
}
33+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.grid.sample
2+
3+
import io.github.cdimascio.dotenv.dotenv
4+
5+
object Config {
6+
private val dotenv = dotenv {
7+
directory = "./"
8+
ignoreIfMalformed = true
9+
ignoreIfMissing = true
10+
}
11+
12+
val apiTokenId: String = getEnvVar("GRID_API_TOKEN_ID")
13+
val apiClientSecret: String = getEnvVar("GRID_API_CLIENT_SECRET")
14+
val webhookPublicKey: String = getEnvVar("GRID_WEBHOOK_PUBLIC_KEY").replace("\\n", "\n")
15+
16+
private fun getEnvVar(key: String): String =
17+
System.getProperty(key)
18+
?: dotenv[key]
19+
?: System.getenv(key)
20+
?: throw IllegalStateException("$key environment variable not set")
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.grid.sample
2+
3+
import com.grid.api.client.GridClient
4+
import com.grid.api.client.okhttp.GridOkHttpClient
5+
6+
object GridClientBuilder {
7+
val client: GridClient by lazy {
8+
GridOkHttpClient.builder()
9+
.username(Config.apiTokenId)
10+
.password(Config.apiClientSecret)
11+
.build()
12+
}
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.grid.sample
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.fasterxml.jackson.databind.SerializationFeature
5+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
6+
7+
object JsonUtils {
8+
val mapper: ObjectMapper = jacksonObjectMapper().apply {
9+
enable(SerializationFeature.INDENT_OUTPUT)
10+
}
11+
12+
fun prettyPrint(obj: Any): String =
13+
try {
14+
mapper.writeValueAsString(obj)
15+
} catch (e: Exception) {
16+
"""{"error": "Failed to serialize response: ${e.message}"}"""
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.grid.sample
2+
3+
import kotlinx.coroutines.flow.MutableSharedFlow
4+
import kotlinx.coroutines.flow.SharedFlow
5+
import kotlinx.coroutines.flow.asSharedFlow
6+
7+
object WebhookStream {
8+
private val _eventFlow = MutableSharedFlow<String>(replay = 10)
9+
val eventFlow: SharedFlow<String> = _eventFlow.asSharedFlow()
10+
11+
fun addEvent(event: String) {
12+
println("Broadcasting webhook: $event")
13+
_eventFlow.tryEmit(event)
14+
}
15+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.grid.sample.routes
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.grid.api.models.customers.CustomerCreateParams
5+
import com.grid.api.models.customers.CustomerCreateParams.CreateCustomerRequest
6+
import com.grid.api.models.customers.CustomerType
7+
import com.grid.sample.GridClientBuilder
8+
import com.grid.sample.JsonUtils
9+
import io.ktor.http.*
10+
import io.ktor.server.request.*
11+
import io.ktor.server.response.*
12+
import io.ktor.server.routing.*
13+
14+
fun Route.customerRoutes() {
15+
route("/api/customers") {
16+
post {
17+
try {
18+
val body = call.receiveText()
19+
val json = JsonUtils.mapper.readTree(body)
20+
21+
val individualRequest = CreateCustomerRequest
22+
.IndividualCustomerCreateRequest.builder()
23+
.customerType(CustomerType.INDIVIDUAL)
24+
.platformCustomerId(json.requiredText("platformCustomerId"))
25+
.apply {
26+
json.optText("fullName")?.let { fullName(it) }
27+
json.optText("nationality")?.let { nationality(it) }
28+
}
29+
.build()
30+
31+
val params = CustomerCreateParams.builder()
32+
.createCustomerRequest(
33+
CreateCustomerRequest.ofIndividualCustomerCreate(individualRequest)
34+
)
35+
.build()
36+
37+
val customer = GridClientBuilder.client.customers().create(params)
38+
call.respondText(
39+
JsonUtils.prettyPrint(customer),
40+
ContentType.Application.Json,
41+
HttpStatusCode.Created
42+
)
43+
} catch (e: Exception) {
44+
call.respondText(
45+
"""{"error": "${e.message}"}""",
46+
ContentType.Application.Json,
47+
HttpStatusCode.InternalServerError
48+
)
49+
}
50+
}
51+
}
52+
}
53+
54+
private fun JsonNode.optText(field: String): String? =
55+
if (has(field) && !get(field).isNull) get(field).asText() else null
56+
57+
private fun JsonNode.requiredText(field: String): String =
58+
optText(field) ?: throw IllegalArgumentException("$field is required")
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.grid.sample.routes
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.grid.api.models.customers.externalaccounts.BaseExternalAccountInfo
5+
import com.grid.api.models.customers.externalaccounts.BeneficiaryOneOf
6+
import com.grid.api.models.customers.externalaccounts.ExternalAccountCreate
7+
import com.grid.api.models.customers.externalaccounts.ExternalAccountCreateParams
8+
import com.grid.api.models.customers.externalaccounts.ExternalAccountInfoOneOf
9+
import com.grid.api.models.customers.externalaccounts.IndividualBeneficiary
10+
import com.grid.api.models.customers.externalaccounts.BaseBeneficiary
11+
import com.grid.api.models.customers.externalaccounts.UsAccountInfo
12+
import com.grid.sample.GridClientBuilder
13+
import com.grid.sample.JsonUtils
14+
import io.ktor.http.*
15+
import io.ktor.server.request.*
16+
import io.ktor.server.response.*
17+
import io.ktor.server.routing.*
18+
import java.time.LocalDate
19+
20+
fun Route.externalAccountRoutes() {
21+
route("/api/customers/{customerId}/external-accounts") {
22+
post {
23+
try {
24+
val customerId = call.parameters["customerId"]
25+
?: return@post call.respondText(
26+
"""{"error": "customerId is required"}""",
27+
ContentType.Application.Json,
28+
HttpStatusCode.BadRequest
29+
)
30+
31+
val body = call.receiveText()
32+
val json = JsonUtils.mapper.readTree(body)
33+
val accountInfo = json.get("accountInfo")
34+
35+
val beneficiaryNode = json.get("beneficiary")
36+
val beneficiary = if (beneficiaryNode != null && !beneficiaryNode.isNull) {
37+
BeneficiaryOneOf.ofIndividualBeneficiary(
38+
IndividualBeneficiary.builder()
39+
.beneficiaryType(BaseBeneficiary.BeneficiaryType.INDIVIDUAL)
40+
.fullName(beneficiaryNode.optText("fullName") ?: "")
41+
.nationality(beneficiaryNode.optText("nationality") ?: "US")
42+
.birthDate(LocalDate.parse(
43+
beneficiaryNode.optText("birthDate") ?: "1990-01-01"
44+
))
45+
.build()
46+
)
47+
} else {
48+
BeneficiaryOneOf.ofIndividualBeneficiary(
49+
IndividualBeneficiary.builder()
50+
.beneficiaryType(BaseBeneficiary.BeneficiaryType.INDIVIDUAL)
51+
.fullName("Account Holder")
52+
.nationality("US")
53+
.birthDate(LocalDate.parse("1990-01-01"))
54+
.build()
55+
)
56+
}
57+
58+
val accountCategory = when (accountInfo.optText("accountType")?.uppercase()) {
59+
"SAVINGS" -> UsAccountInfo.AccountCategory.SAVINGS
60+
else -> UsAccountInfo.AccountCategory.CHECKING
61+
}
62+
63+
val usAccountInfo = ExternalAccountInfoOneOf
64+
.UsAccountExternalAccountInfo.builder()
65+
.accountType(BaseExternalAccountInfo.AccountType.US_ACCOUNT)
66+
.accountCategory(accountCategory)
67+
.accountNumber(accountInfo.get("accountNumber").asText())
68+
.routingNumber(accountInfo.get("routingNumber").asText())
69+
.beneficiary(beneficiary)
70+
.build()
71+
72+
val externalAccountCreate = ExternalAccountCreate.builder()
73+
.accountInfo(
74+
ExternalAccountInfoOneOf.ofUsAccountExternalAccountInfo(usAccountInfo)
75+
)
76+
.currency(json.optText("currency") ?: "USD")
77+
.customerId(customerId)
78+
.apply {
79+
json.optText("platformAccountId")?.let { platformAccountId(it) }
80+
}
81+
.build()
82+
83+
val params = ExternalAccountCreateParams.builder()
84+
.externalAccountCreate(externalAccountCreate)
85+
.build()
86+
87+
val account = GridClientBuilder.client.customers().externalAccounts().create(params)
88+
call.respondText(
89+
JsonUtils.prettyPrint(account),
90+
ContentType.Application.Json,
91+
HttpStatusCode.Created
92+
)
93+
} catch (e: Exception) {
94+
call.respondText(
95+
"""{"error": "${e.message}"}""",
96+
ContentType.Application.Json,
97+
HttpStatusCode.InternalServerError
98+
)
99+
}
100+
}
101+
}
102+
}
103+
104+
private fun JsonNode.optText(field: String): String? =
105+
if (has(field) && !get(field).isNull) get(field).asText() else null

0 commit comments

Comments
 (0)