Skip to content

Commit

Permalink
feat(vault): vault admin api for user registration
Browse files Browse the repository at this point in the history
  • Loading branch information
vihangpatil committed Jun 28, 2024
1 parent 48a9ee2 commit 8e2b6b1
Show file tree
Hide file tree
Showing 8 changed files with 515 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,59 @@ class VaultAppTest : BehaviorSpec({
}
}
}

suspend fun getVaultUserStatus(): HttpResponse {
return adminApiClient.get {
url(path = "/apps/vault/admin/user") {
parameters.append("email", "test@k33.com")
}
}
}

suspend fun registerVaultUser(vaultAccountId: String): HttpResponse {
return adminApiClient.put {
url(path = "/apps/vault/admin/user") {
parameters.append("email", "test@k33.com")
parameters.append("vaultAccountId", vaultAccountId)
}
}
}

given("For vault admin, user is not registered") {
`when`("GET /apps/vault/admin/user") {
val response = getVaultUserStatus()
then("Status should be 404 NOT FOUND") {
response.status shouldBe HttpStatusCode.NotFound
response.body<VaultUserStatus>() shouldBe VaultUserStatus(
platformRegistered = true,
vaultAccountId = null,
stripeErrors = emptyList(),
)
}
}
`when`("Register vault user - PUT /apps/vault/admin/user") {
val response = registerVaultUser(vaultAccountId = "76")
then("Status should be 200") {
response.status shouldBe HttpStatusCode.OK
response.body<VaultUserStatus>() shouldBe VaultUserStatus(
platformRegistered = true,
vaultAccountId = "76",
stripeErrors = emptyList(),
)
}
and("GET /apps/vault/admin/user") {
val updatedResponse = getVaultUserStatus()
then("Status should be 200") {
updatedResponse.status shouldBe HttpStatusCode.OK
response.body<VaultUserStatus>() shouldBe VaultUserStatus(
platformRegistered = true,
vaultAccountId = "76",
stripeErrors = emptyList(),
)
}
}
}
}
})

@Serializable
Expand Down Expand Up @@ -220,4 +273,11 @@ data class VaultAppSettings(
data class VaultApp(
val vaultAccountId: String,
val currency: String,
)

@Serializable
data class VaultUserStatus(
val platformRegistered: Boolean,
val vaultAccountId: String? = null,
val stripeErrors: List<String>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.ktor.server.request.receive
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.put
import io.ktor.server.routing.route
Expand Down Expand Up @@ -98,6 +99,61 @@ fun Application.module() {
}
}

route("/apps/vault/admin/user") {
get {
val email = call.request.queryParameters["email"]
?: throw BadRequestException("Missing query parameter: email")
logWithMDC("vaultUserEmail" to email) {
val vaultUserStatus = VaultService.getUserStatus(email = email)
val httpStatusCode = if (vaultUserStatus.platformRegistered
&& vaultUserStatus.vaultAccountId != null
&& vaultUserStatus.stripeErrors.isEmpty()
) {
HttpStatusCode.OK
} else {
HttpStatusCode.NotFound
}
call.respond(httpStatusCode, vaultUserStatus)
}
}
put {
val email = call.request.queryParameters["email"]
?: throw BadRequestException("Missing query parameter: email")
val vaultAccountId = call.request.queryParameters["vaultAccountId"]
?: throw BadRequestException("Missing query parameter: vaultAccountId")
val currency = call.request.queryParameters["currency"] ?: "USD"
logWithMDC("vaultUserEmail" to email) {
val vaultUserStatus = VaultService.register(
email = email,
vaultAccountId = vaultAccountId,
currency = currency,
)
val httpStatusCode = if (vaultUserStatus.platformRegistered
&& vaultUserStatus.vaultAccountId != null
&& vaultUserStatus.stripeErrors.isEmpty()
) {
HttpStatusCode.OK
} else {
HttpStatusCode.NotFound
}
call.respond(httpStatusCode, vaultUserStatus)
}
}
delete {
val email = call.request.queryParameters["email"]
?: throw BadRequestException("Missing query parameter: email")
logWithMDC("vaultUserEmail" to email) {
val vaultUserStatus = VaultService.deregister(email = email)
val httpStatusCode = if (vaultUserStatus.platformRegistered) {
HttpStatusCode.OK
} else {
HttpStatusCode.NotFound
}
call.respond(httpStatusCode, vaultUserStatus)
}
}
}

get("/apps/vault/admin/transactions") {
val vaultAccountId = call.request.queryParameters["vaultAccountId"]
?: throw BadRequestException("Missing query parameter: vaultAccountId")
Expand Down Expand Up @@ -246,4 +302,11 @@ data class Transaction(
val amountUSD: String,
val feeCurrency: String,
val fee: String,
)

@Serializable
data class VaultUserStatus(
val platformRegistered: Boolean,
val vaultAccountId: String?,
val stripeErrors: List<String>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import com.k33.platform.app.vault.coingecko.CoinGeckoClient
import com.k33.platform.app.vault.pdf.getBalanceReport
import com.k33.platform.app.vault.stripe.StripeService
import com.k33.platform.filestore.FileStoreService
import com.k33.platform.fireblocks.service.Destination
import com.k33.platform.fireblocks.service.Destinations
import com.k33.platform.fireblocks.service.FireblocksService
import com.k33.platform.fireblocks.service.TxnSrcDest
import com.k33.platform.identity.auth.gcp.FirebaseAuthService
import com.k33.platform.user.UserId
import com.k33.platform.utils.logging.NotifySlack
import com.k33.platform.utils.logging.getLogger
import com.stripe.model.Address
import io.firestore4k.typed.FirestoreClient
import io.firestore4k.typed.div
import io.ktor.server.plugins.NotFoundException
Expand Down Expand Up @@ -151,6 +150,10 @@ object VaultService {
firestoreClient.put(inVaultAppContext(), vaultApp)
}

suspend fun UserId.deregister() {
firestoreClient.delete(inVaultAppContext())
}

suspend fun UserId.getTransactions(
dateRange: Pair<Instant, Instant>,
zoneId: ZoneId,
Expand Down Expand Up @@ -178,6 +181,88 @@ object VaultService {
)
}

suspend fun getUserStatus(
email: String,
): VaultUserStatus {
val stripeErrors = StripeService.getCustomerDetails(email = email).validate()
val userId = FirebaseAuthService.findUserIdOrNull(email)
?.let(::UserId)
?: return VaultUserStatus(
platformRegistered = false,
vaultAccountId = null,
stripeErrors = stripeErrors,
)
val vaultAccountId = firestoreClient.get(userId.inVaultAppContext())
?.vaultAccountId
?: return VaultUserStatus(
platformRegistered = true,
vaultAccountId = null,
stripeErrors = stripeErrors,
)
return VaultUserStatus(
platformRegistered = true,
vaultAccountId = vaultAccountId,
stripeErrors = stripeErrors,
)
}

suspend fun register(
email: String,
vaultAccountId: String,
currency: String,
): VaultUserStatus {
val stripeErrors = StripeService.getCustomerDetails(email = email).validate()
val userId = FirebaseAuthService.findUserIdOrNull(email)
?.let(::UserId)
?: return VaultUserStatus(
platformRegistered = false,
vaultAccountId = null,
stripeErrors = stripeErrors,
)
userId.register(
VaultApp(
vaultAccountId = vaultAccountId,
currency = currency,
)
)
return VaultUserStatus(
platformRegistered = true,
vaultAccountId = vaultAccountId,
stripeErrors = stripeErrors,
)
}

suspend fun deregister(
email: String,
): VaultUserStatus {
val stripeErrors = StripeService.getCustomerDetails(email = email).validate()
val userId = FirebaseAuthService.findUserIdOrNull(email)
?.let(::UserId)
?: return VaultUserStatus(
platformRegistered = false,
vaultAccountId = null,
stripeErrors = stripeErrors,
)
userId.deregister()
return VaultUserStatus(
platformRegistered = true,
vaultAccountId = null,
stripeErrors = stripeErrors,
)
}

private fun List<StripeService.CustomerDetails>.validate(): List<String> = buildList {
if (this@validate.isEmpty()) {
add("No stripe users found with this email")
} else if (this@validate.size > 1) {
add("Multiple stripe users found with this email")
} else {
this@validate.single().address.validationErrors().forEach {
add("Missing address field: $it")
}
}
}

suspend fun getTransactions(
vaultAccountId: String,
dateRange: Pair<Instant, Instant>,
Expand Down Expand Up @@ -217,6 +302,7 @@ object VaultService {
amountUSD = destinations?.amountUSD ?: ""
"CREDIT"
}

else -> ""
}

Expand Down Expand Up @@ -325,6 +411,15 @@ object VaultService {
}
}
.map { (userId, vaultAssets) ->
val validationErrors = userIdToDetailsMap[userId]?.address?.validationErrors() ?: listOf("address")
if (validationErrors.isNotEmpty()) {
logger.warn(
NotifySlack.ALERTS,
"Missing stripe address field(s) for user: {}, field(s): {}",
userId,
validationErrors
)
}
val reportFileContents = getBalanceReport(
name = userIdToDetailsMap[userId]?.name ?: "",
address = userIdToDetailsMap[userId]?.address?.let { address ->
Expand All @@ -348,6 +443,23 @@ object VaultService {
}
}

private fun Address.validationErrors(): List<String> = buildList {
if (line1.isNullOrEmpty()) {
add("line1")
}
if (postalCode.isNullOrEmpty()) {
add("postalCode")
}
if (city.isNullOrEmpty()) {
add("city")
}
if (country.isNullOrEmpty()) {
add("country")
} else if (Locale.of("", country).displayName.isNullOrEmpty()) {
add("country locale")
}
}

private suspend fun fetchFireblocksVaultAssets(
vaultAccountId: String,
): List<VaultAssetBalance> = coroutineScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,27 @@ object StripeService {
}
?: emptyList()
}

suspend fun getCustomerDetails(
email: String,
): List<CustomerDetails> {
val listParams = CustomerListParams
.builder()
.setEmail(email)
.setLimit(100)
.build()

return stripeClient.call {
Customer.list(listParams, requestOptions)
}
?.data
?.map {
CustomerDetails(
name = it.name,
address = it.address,
email = it.email,
)
}
?: emptyList()
}
}
15 changes: 15 additions & 0 deletions libs/clients/k33-backend-client/src/main/httpx/vault.http
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,19 @@ GET https://api.k33.com/apps/vault/admin/transactions
?vaultAccountId={{vault_account_id}}
&afterDate=2023-01-01
&beforeDate=2023-12-31
X-API-KEY: {{admin_api_key}}

### Check vault user status by admin

GET https://api.k33.com/apps/vault/admin/user?email={{$random.email}}
X-API-KEY: {{admin_api_key}}

### Register vault user by admin

GET https://api.k33.com/apps/vault/admin/user?email={{$random.email}}&vaultAccountId={{$random.integer()}}&currency=USD
X-API-KEY: {{admin_api_key}}

### Deregister vault user by admin

DELETE https://api.k33.com/apps/vault/admin/user?email={{$random.email}}
X-API-KEY: {{admin_api_key}}
Loading

0 comments on commit 8e2b6b1

Please sign in to comment.