From b14190e970bf641faa7e38a68a2f5dc3376b9fe7 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 22 Jul 2025 22:59:19 -0700 Subject: [PATCH 1/4] Remove full input logging --- .../src/main/kotlin/com/zenobiapay/bank/handlers/BankHandler.kt | 2 +- .../kotlin/com/zenobiapay/transfer/handlers/TransferHandler.kt | 2 +- .../src/main/kotlin/com/zenobiapay/user/handlers/UserHandler.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/handlers/BankHandler.kt b/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/handlers/BankHandler.kt index c1962cd..5d85b65 100644 --- a/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/handlers/BankHandler.kt +++ b/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/handlers/BankHandler.kt @@ -45,7 +45,7 @@ class BankHandler : RequestHandler createLinkTokenOperation "/exchange-token" -> exchangeTokenOperation diff --git a/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/handlers/TransferHandler.kt b/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/handlers/TransferHandler.kt index 68dc176..600f77c 100644 --- a/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/handlers/TransferHandler.kt +++ b/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/handlers/TransferHandler.kt @@ -73,7 +73,7 @@ class TransferHandler : RequestHandler createTransferRequestOperation "/fulfill-transfer" -> fulfillTransferOperation diff --git a/kotlin/lambda/user-handler/src/main/kotlin/com/zenobiapay/user/handlers/UserHandler.kt b/kotlin/lambda/user-handler/src/main/kotlin/com/zenobiapay/user/handlers/UserHandler.kt index 9a0d146..0248790 100644 --- a/kotlin/lambda/user-handler/src/main/kotlin/com/zenobiapay/user/handlers/UserHandler.kt +++ b/kotlin/lambda/user-handler/src/main/kotlin/com/zenobiapay/user/handlers/UserHandler.kt @@ -57,7 +57,7 @@ class UserHandler : RequestHandler updateMerchantConfigOperation "/get-merchant-config" -> getMerchantConfigOperation From ef11f17db1286884ea2baf4063f511241f9fa0d0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 22 Jul 2025 23:31:14 -0700 Subject: [PATCH 2/4] Include bankAccountId of merchant on payout in transfer update --- .../kotlin/com/zenobiapay/payout/handlers/PayoutProcessor.kt | 1 + .../zenobiapay/transfer/operations/EditTransferOperation.kt | 2 -- .../kotlin/com/zenobiapay/table/transfer/dao/TransferDao.kt | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/kotlin/lambda/payout-handler/src/main/kotlin/com/zenobiapay/payout/handlers/PayoutProcessor.kt b/kotlin/lambda/payout-handler/src/main/kotlin/com/zenobiapay/payout/handlers/PayoutProcessor.kt index 4c48bd7..bf9ab10 100644 --- a/kotlin/lambda/payout-handler/src/main/kotlin/com/zenobiapay/payout/handlers/PayoutProcessor.kt +++ b/kotlin/lambda/payout-handler/src/main/kotlin/com/zenobiapay/payout/handlers/PayoutProcessor.kt @@ -167,6 +167,7 @@ class PayoutProcessor : RequestHandler, Unit> { logger.info { "Payout complete. Marking transfer as paid out." } transferDao.updateTransferPaidOut( transferItem, + bankAccountId, fee, transferResponse?.transfer?.id, payoutTime, diff --git a/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/EditTransferOperation.kt b/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/EditTransferOperation.kt index adf2f02..6ba0f38 100644 --- a/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/EditTransferOperation.kt +++ b/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/EditTransferOperation.kt @@ -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") diff --git a/kotlin/shared/table/transfer/src/main/kotlin/com/zenobiapay/table/transfer/dao/TransferDao.kt b/kotlin/shared/table/transfer/src/main/kotlin/com/zenobiapay/table/transfer/dao/TransferDao.kt index ad108c3..fee10c7 100644 --- a/kotlin/shared/table/transfer/src/main/kotlin/com/zenobiapay/table/transfer/dao/TransferDao.kt +++ b/kotlin/shared/table/transfer/src/main/kotlin/com/zenobiapay/table/transfer/dao/TransferDao.kt @@ -177,6 +177,7 @@ class TransferDao @Inject constructor( fun updateTransferPaidOut( transferItem: TransferItem, + merchantBankAccountId: String, fee: Int?, orumPayoutId: String?, payoutTime: Instant, @@ -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, From a6cb6bb120516bfcca982fdb9617cc929fc9269c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 23 Jul 2025 10:21:40 -0700 Subject: [PATCH 3/4] Add refresh exception --- .../bank/operations/CreateLinkTokenOperation.kt | 9 ++++++++- .../model/exception/PlaidRefreshRequiredException.kt | 3 +++ .../kotlin/com/zenobiapay/api/util/ResponseHandler.kt | 6 ++---- kotlin/shared/plaid/build.gradle.kts | 1 + .../main/kotlin/com/zenobiapay/plaid/PlaidWrapper.kt | 10 +++++++++- openapi.yml | 7 +++++++ sam/lambda-stack.yml | 7 +++++++ 7 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 kotlin/shared/api/model/src/main/kotlin/com/zenobiapay/api/model/exception/PlaidRefreshRequiredException.kt diff --git a/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/operations/CreateLinkTokenOperation.kt b/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/operations/CreateLinkTokenOperation.kt index 2fa216a..07d515a 100644 --- a/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/operations/CreateLinkTokenOperation.kt +++ b/kotlin/lambda/bank-handler/src/main/kotlin/com/zenobiapay/bank/operations/CreateLinkTokenOperation.kt @@ -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 @@ -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() { @@ -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) diff --git a/kotlin/shared/api/model/src/main/kotlin/com/zenobiapay/api/model/exception/PlaidRefreshRequiredException.kt b/kotlin/shared/api/model/src/main/kotlin/com/zenobiapay/api/model/exception/PlaidRefreshRequiredException.kt new file mode 100644 index 0000000..bd6db4d --- /dev/null +++ b/kotlin/shared/api/model/src/main/kotlin/com/zenobiapay/api/model/exception/PlaidRefreshRequiredException.kt @@ -0,0 +1,3 @@ +package com.zenobiapay.api.model.exception + +class PlaidRefreshRequiredException: ZenobiaExternalException("Plaid refresh required") diff --git a/kotlin/shared/api/src/main/kotlin/com/zenobiapay/api/util/ResponseHandler.kt b/kotlin/shared/api/src/main/kotlin/com/zenobiapay/api/util/ResponseHandler.kt index 359ee37..f376c65 100644 --- a/kotlin/shared/api/src/main/kotlin/com/zenobiapay/api/util/ResponseHandler.kt +++ b/kotlin/shared/api/src/main/kotlin/com/zenobiapay/api/util/ResponseHandler.kt @@ -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 {} @@ -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 diff --git a/kotlin/shared/plaid/build.gradle.kts b/kotlin/shared/plaid/build.gradle.kts index 6f168c7..acabd35 100644 --- a/kotlin/shared/plaid/build.gradle.kts +++ b/kotlin/shared/plaid/build.gradle.kts @@ -26,6 +26,7 @@ repositories { } dependencies { + api(project(":kotlin:shared:api:model")) api(project(":kotlin:shared:metrics")) api(libs.kotlin.stdlib) diff --git a/kotlin/shared/plaid/src/main/kotlin/com/zenobiapay/plaid/PlaidWrapper.kt b/kotlin/shared/plaid/src/main/kotlin/com/zenobiapay/plaid/PlaidWrapper.kt index 6db8e1c..0a00cc8 100644 --- a/kotlin/shared/plaid/src/main/kotlin/com/zenobiapay/plaid/PlaidWrapper.kt +++ b/kotlin/shared/plaid/src/main/kotlin/com/zenobiapay/plaid/PlaidWrapper.kt @@ -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 @@ -48,7 +49,7 @@ class PlaidWrapper @Inject constructor( @Named(IS_PLAID_SANDBOX) private val isPlaidSandbox: Boolean, ) { - fun createLinkToken(userId: String, webhookUrl: String, product: List): LinkTokenCreateResponse { + fun createLinkToken(userId: String, webhookUrl: String, product: List, userToken: String?): LinkTokenCreateResponse { val user = LinkTokenCreateRequestUser() .clientUserId(userId) @@ -64,6 +65,7 @@ class PlaidWrapper @Inject constructor( .language("en") .accountFilters(accountFilters) .webhook(webhookUrl) + .userToken(userToken) .redirectUri("https://zenobiapay.com/plaid") return getResponseOrThrowException("CreateLinkToken") { @@ -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()}") } } diff --git a/openapi.yml b/openapi.yml index 0593bd8..c7d7756 100644 --- a/openapi.yml +++ b/openapi.yml @@ -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" diff --git a/sam/lambda-stack.yml b/sam/lambda-stack.yml index c8f5fd3..acb5371 100644 --- a/sam/lambda-stack.yml +++ b/sam/lambda-stack.yml @@ -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" From 5dcad21b413b2695b11f0835854c9faf2203b540 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 25 Jul 2025 10:46:29 -0700 Subject: [PATCH 4/4] Remove orum call on fulfill --- .../transfer/operations/FulfillTransferOperation.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/FulfillTransferOperation.kt b/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/FulfillTransferOperation.kt index 9e981e7..d269c26 100644 --- a/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/FulfillTransferOperation.kt +++ b/kotlin/lambda/transfer-handler/src/main/kotlin/com/zenobiapay/transfer/operations/FulfillTransferOperation.kt @@ -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(