Skip to content
Merged

Beta #67

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class BankHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyR
}

override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent {
logger.info { "Got input $input" }
logger.info { "Got input ${input?.body}" }
val operation = when (input?.path) {
"/create-link-token" -> createLinkTokenOperation
"/exchange-token" -> exchangeTokenOperation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.zenobiapay.api.operation.Operation
import com.zenobiapay.api.model.cognito.UserPoolGroup
import com.zenobiapay.bank.di.API_GATEWAY_ENDPOINT
import com.zenobiapay.plaid.PlaidWrapper
import com.zenobiapay.table.bank.dao.BankDao
import com.zenobiapay.table.user.dao.UserDao
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
Expand All @@ -20,6 +21,7 @@ private val logger = KotlinLogging.logger {}
class CreateLinkTokenOperation @Inject constructor(
private val plaidWrapper: PlaidWrapper,
private val userDao: UserDao,
private val bankDao: BankDao,
@Named(API_GATEWAY_ENDPOINT) private val apiGatewayEndpoint: String,
): Operation<CreateLinkTokenRequest, CreateLinkToken200Response>() {

Expand All @@ -34,7 +36,12 @@ class CreateLinkTokenOperation @Inject constructor(
}
val plaidWebhook = apiGatewayEndpoint + "plaid-webhook"
logger.info { "Using plaid webhook $plaidWebhook" }
val response = plaidWrapper.createLinkToken(sub, plaidWebhook, getPlaidProducts(request.product))

val userToken = if (request.isRefresh && userId != null && request.bankAccountId != null && request.deviceId != null) {
bankDao.getBankAccount(bankAccountId = request.bankAccountId, userId = userId, deviceId = request.deviceId).accessToken
} else null

val response = plaidWrapper.createLinkToken(sub, plaidWebhook, getPlaidProducts(request.product), userToken)
return CreateLinkToken200Response()
.linkToken(response.linkToken)
.sub(sub)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class PayoutProcessor : RequestHandler<Map<String, Any>, Unit> {
logger.info { "Payout complete. Marking transfer as paid out." }
transferDao.updateTransferPaidOut(
transferItem,
bankAccountId,
fee,
transferResponse?.transfer?.id,
payoutTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class TransferHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayPr
}

override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent {
logger.info { "Got input $input" }
logger.info { "Got input ${input?.body}" }
val operation = when (input?.path) {
"/create-transfer-request" -> createTransferRequestOperation
"/fulfill-transfer" -> fulfillTransferOperation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ class EditTransferOperation @Inject constructor(
val merchantPayout = transfer.amount!! - getFee(transfer.amount!!)
logger.info { "Calculated merchant payout as $merchantPayout" }

logger.info { "Initiating refund of $refundAmount cents from merchant ${merchantIdentity.id} to customer ${customerIdentity.id}" }

val customerReferenceId = generateCustomerOrumId(customerIdentity.id)
val merchantItem = userDao.getUserItem(merchantIdentity.id) ?: throw ResourceNotFoundException("MERCHANT")
val merchantReferenceId = merchantItem.data.orumReferenceId ?: throw ResourceNotFoundException("ORUM REFERENCE ID")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ class FulfillTransferOperation @Inject constructor(

logger.info { "Successfully set request to FULFILL_LOCKED" }
val fulfillTimestamp = Instant.now()
transferFunds(
transferRequestId,
transferAmount,
creditorId,
merchantItem.data.merchantData?.displayName
)
// transferFunds(
// transferRequestId,
// transferAmount,
// creditorId,
// merchantItem.data.merchantData?.displayName
// )

val statementItems = transferRequestData.statementItems.map { it.toApiStatementItem() }
transferDao.updateTransferRequestFulfilled(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class UserHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyR
}

override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent {
logger.info { "Got input $input" }
logger.info { "Got input ${input?.body}" }
val operation = when (input?.path) {
"/update-merchant-config" -> updateMerchantConfigOperation
"/get-merchant-config" -> getMerchantConfigOperation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.zenobiapay.api.model.exception

class PlaidRefreshRequiredException: ZenobiaExternalException("Plaid refresh required")
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@ import com.zenobiapay.api.generated.model.ErrorResponse
import com.zenobiapay.api.model.exception.DeclinedException
import com.zenobiapay.api.model.exception.InsufficientFundsException
import com.zenobiapay.api.model.exception.InvalidRequestException
import com.zenobiapay.api.model.exception.PlaidRefreshRequiredException
import com.zenobiapay.api.model.exception.TransferStatusException
import com.zenobiapay.api.operation.Operation
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.validation.Validation
import jakarta.validation.Validator
import org.apache.logging.log4j.ThreadContext
import jakarta.inject.Inject
import java.time.Instant
import kotlin.time.Duration

private val logger = KotlinLogging.logger {}

Expand Down Expand Up @@ -84,6 +81,7 @@ class ResponseHandler @Inject constructor(

fun generateApiGatewayErrorResponse(error: Exception, path: String? = null): APIGatewayProxyResponseEvent {
val (errorCode, status) = when (error) {
is PlaidRefreshRequiredException-> 400 to "Plaid refresh required"
is ResourceNotFoundException, is UnknownPathException -> 404 to error.message
is UnauthorizedException -> 403 to error.message
is ServiceQuotaExceededException -> 429 to error.message
Expand Down
1 change: 1 addition & 0 deletions kotlin/shared/plaid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
}

dependencies {
api(project(":kotlin:shared:api:model"))
api(project(":kotlin:shared:metrics"))

api(libs.kotlin.stdlib)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.plaid.client.model.WebhookVerificationKeyGetRequest
import com.plaid.client.model.WebhookVerificationKeyGetResponse
import com.plaid.client.request.PlaidApi
import com.zenobia.metric.MetricHelper
import com.zenobiapay.api.model.exception.PlaidRefreshRequiredException
import com.zenobiapay.plaid.di.IS_PLAID_SANDBOX
import com.zenobiapay.plaid.model.SignalResult
import io.github.oshai.kotlinlogging.KotlinLogging
Expand All @@ -48,7 +49,7 @@ class PlaidWrapper @Inject constructor(
@Named(IS_PLAID_SANDBOX)
private val isPlaidSandbox: Boolean,
) {
fun createLinkToken(userId: String, webhookUrl: String, product: List<Products>): LinkTokenCreateResponse {
fun createLinkToken(userId: String, webhookUrl: String, product: List<Products>, userToken: String?): LinkTokenCreateResponse {
val user = LinkTokenCreateRequestUser()
.clientUserId(userId)

Expand All @@ -64,6 +65,7 @@ class PlaidWrapper @Inject constructor(
.language("en")
.accountFilters(accountFilters)
.webhook(webhookUrl)
.userToken(userToken)
.redirectUri("https://zenobiapay.com/plaid")

return getResponseOrThrowException("CreateLinkToken") {
Expand Down Expand Up @@ -189,6 +191,12 @@ class PlaidWrapper @Inject constructor(
if (response.isSuccessful) {
return@withXray response.body()!!
}
if (response.code() == 400) {
val errorBody = response.errorBody()?.string()
if (errorBody?.contains("ITEM_LOGIN_REQUIRED") == true) {
throw PlaidRefreshRequiredException()
}
}
throw PlaidException("Failed to call $operationName. Error code ${response.code()}, body ${response.errorBody()?.string()}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ class TransferDao @Inject constructor(

fun updateTransferPaidOut(
transferItem: TransferItem,
merchantBankAccountId: String,
fee: Int?,
orumPayoutId: String?,
payoutTime: Instant,
Expand All @@ -188,6 +189,9 @@ class TransferDao @Inject constructor(
fee = fee,
orumPayoutId = orumPayoutId,
payoutTime = payoutTime.toString(),
merchant = transferItem.data?.merchant?.copy(
bankAccountId = merchantBankAccountId
)
),
version = version,
ttl = null,
Expand Down
7 changes: 7 additions & 0 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ paths:
type: string
enum:
- AUTH
isRefresh: # Fields below only needed if isRefresh is true
type: boolean
default: false
bankAccountId:
type: string
deviceId:
type: string
responses:
"200":
description: "Successful response"
Expand Down
7 changes: 7 additions & 0 deletions sam/lambda-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,13 @@ Resources:
type: string
enum:
- AUTH
isRefresh: # Fields below only needed if isRefresh is true
type: boolean
default: false
bankAccountId:
type: string
deviceId:
type: string
responses:
"200":
description: "Successful response"
Expand Down
Loading