feature/add HOLD transaction request type and related models for v6.0.0

This commit is contained in:
hongwei 2025-10-05 22:18:59 +02:00
parent f4253b652c
commit 121ab9edae
6 changed files with 155 additions and 7 deletions

View File

@ -362,7 +362,7 @@ transactionRequests_enabled=false
transactionRequests_connector=mapped
## Transaction Request Types that are supported on this server. Possible values might include SANDBOX_TAN, COUNTERPARTY, SEPA, FREE_FORM
transactionRequests_supported_types=SANDBOX_TAN,COUNTERPARTY,SEPA,ACCOUNT_OTP,ACCOUNT,SIMPLE
transactionRequests_supported_types=SANDBOX_TAN,COUNTERPARTY,SEPA,ACCOUNT_OTP,ACCOUNT,SIMPLE,HOLD
## Transaction request challenge threshold. Level at which challenge is created and needs to be answered.
## The Currency is EUR unless set with transactionRequests_challenge_currency.
@ -1071,6 +1071,7 @@ database_messages_scheduler_interval=3600
# -- SCA (Strong Customer Authentication) method for OTP challenge-------
# ACCOUNT_OTP_INSTRUCTION_TRANSPORT=DUMMY
# SIMPLE_OTP_INSTRUCTION_TRANSPORT=DUMMY
# HOLD_OTP_INSTRUCTION_TRANSPORT=DUMMY
# SEPA_OTP_INSTRUCTION_TRANSPORT=DUMMY
# FREE_FORM_OTP_INSTRUCTION_TRANSPORT=DUMMY
# COUNTERPARTY_OTP_INSTRUCTION_TRANSPORT=DUMMY

View File

@ -5811,6 +5811,12 @@ object SwaggerDefinitionsJSON {
description = descriptionExample.value
)
// HOLD sample (V600)
lazy val transactionRequestBodyHoldJsonV600 = TransactionRequestBodyHoldJsonV600(
value = amountOfMoneyJsonV121,
description = descriptionExample.value
)
//The common error or success format.
//Just some helper format to use in Json
case class NotSupportedYet()

View File

@ -1,14 +1,14 @@
package code.api.v6_0_0
import code.api.{APIFailureNewStyle, ObpApiFailure}
import code.api.ObpApiFailure
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.util.APIUtil._
import code.api.util.ApiRole.{canDeleteRateLimiting, canReadCallLimits, canSetCallLimits}
import code.api.util.ApiTag._
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, _}
import code.api.util.FutureUtil.EndpointContext
import code.api.util.{NewStyle, RateLimitingUtil}
import code.api.util.NewStyle.HttpCode
import code.api.util.{NewStyle, RateLimitingUtil}
import code.api.v6_0_0.JSONFactory600.{createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
import code.bankconnectors.LocalMappedConnectorInternal
import code.bankconnectors.LocalMappedConnectorInternal._
@ -19,11 +19,10 @@ import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.common.Full
import net.liftweb.http.rest.RestHelper
import java.text.SimpleDateFormat
import java.util.Date
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
@ -45,6 +44,46 @@ trait APIMethods600 {
val codeContext = CodeContext(staticResourceDocs, apiRelations)
staticResourceDocs += ResourceDoc(
createTransactionRequestHold,
implementedInApiVersion,
nameOf(createTransactionRequestHold),
"POST",
"/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/HOLD/transaction-requests",
"Create Transaction Request (HOLD)",
s"""
|
|Create a transaction request to move funds from the account to its Holding Account.
|If the Holding Account does not exist, it will be created automatically.
|
|${transactionRequestGeneralText}
|
""".stripMargin,
transactionRequestBodyHoldJsonV600,
transactionRequestWithChargeJSON400,
List(
$UserNotLoggedIn,
$BankNotFound,
$BankAccountNotFound,
InsufficientAuthorisationToCreateTransactionRequest,
InvalidTransactionRequestType,
InvalidJsonFormat,
NotPositiveAmount,
InvalidTransactionRequestCurrency,
TransactionDisabled,
UnknownError
),
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2)
)
lazy val createTransactionRequestHold: OBPEndpoint = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
"HOLD" :: "transaction-requests" :: Nil JsonPost json -> _ =>
cc => implicit val ec = EndpointContext(Some(cc))
val transactionRequestType = TransactionRequestType("HOLD")
LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json)
}
staticResourceDocs += ResourceDoc(
getCurrentCallsLimit,
implementedInApiVersion,

View File

@ -35,7 +35,6 @@ import code.api.v3_1_0.{RateLimit, RedisCallLimitJson}
import code.entitlement.Entitlement
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model._
import java.util.Date
case class CardanoPaymentJsonV600(
address: String,
@ -122,6 +121,12 @@ case class TransactionRequestBodyEthSendRawTransactionJsonV600(
description: String
)
// ---------------- HOLD models (V600) ----------------
case class TransactionRequestBodyHoldJsonV600(
value: AmountOfMoneyJsonV121,
description: String
) extends TransactionRequestCommonBodyJSON
case class UserJsonV600(
user_id: String,
email : String,

View File

@ -1,5 +1,6 @@
package code.bankconnectors
import code.accountattribute.AccountAttributeX
import code.api.ChargePolicy
import code.api.Constant._
import code.api.berlin.group.ConstantsBG
@ -13,7 +14,7 @@ import code.api.util.newstyle.ViewNewStyle
import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140
import code.api.v2_1_0._
import code.api.v4_0_0._
import code.api.v6_0_0.{TransactionRequestBodyCardanoJsonV600, TransactionRequestBodyEthSendRawTransactionJsonV600, TransactionRequestBodyEthereumJsonV600}
import code.api.v6_0_0.{TransactionRequestBodyCardanoJsonV600, TransactionRequestBodyEthSendRawTransactionJsonV600, TransactionRequestBodyEthereumJsonV600, TransactionRequestBodyHoldJsonV600}
import code.bankconnectors.ethereum.DecodeRawTx
import code.branches.MappedBranch
import code.fx.fx
@ -801,6 +802,10 @@ object LocalMappedConnectorInternal extends MdcLoggable {
description = transactionRequestBodyEthSendRawTransactionJsonV600.description
)
} yield (transactionRequestBodyEthereum)
case HOLD =>
NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyHoldJsonV600 ", 400, callContext) {
json.extract[TransactionRequestBodyHoldJsonV600]
}
case _ =>
NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) {
json.extract[TransactionRequestBodyCommonJSON]
@ -823,6 +828,30 @@ object LocalMappedConnectorInternal extends MdcLoggable {
})
(createdTransactionRequest, callContext) <- transactionRequestTypeValue match {
case HOLD => {
for {
holdBody <- NewStyle.function.tryons(s"$InvalidJsonFormat It should be $TransactionRequestBodyHoldJsonV600 json format", 400, callContext) {
json.extract[TransactionRequestBodyHoldJsonV600]
}
(holdingAccount, callContext) <- getOrCreateHoldingAccount(bankId, fromAccount, callContext)
transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
write(holdBody)(Serialization.formats(NoTypeHints))
}
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(
u,
viewId,
fromAccount,
holdingAccount,
transactionRequestType,
holdBody,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OBP_TRANSACTION_REQUEST_CHALLENGE),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
callContext)
} yield (createdTransactionRequest, callContext)
}
case REFUND => {
for {
transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
@ -1560,5 +1589,72 @@ object LocalMappedConnectorInternal extends MdcLoggable {
}
}
/**
* Find or create a Holding Account for the given parent account and link via account attributes.
* Rules:
* - Holding account uses the same currency as parent.
* - Holding account type: "HOLDING".
* - Attributes on holding: ACCOUNT_ROLE=HOLDING, PARENT_ACCOUNT_ID=parentAccountId
* - Optional reverse link on parent: HOLDING_ACCOUNT_ID=holdingAccountId
*/
private def getOrCreateHoldingAccount(
bankId: BankId,
parentAccount: BankAccount,
callContext: Option[CallContext]
): Future[(BankAccount, Option[CallContext])] = {
val params = Map("PARENT_ACCOUNT_ID" -> List(parentAccount.accountId.value))
for {
// Query by attribute to find accounts that link to the parent
accountIdsBox <- AccountAttributeX.accountAttributeProvider.vend.getAccountIdsByParams(bankId, params)
accountIds = accountIdsBox.getOrElse(Nil).map(id => AccountId(id))
// Try to find an existing holding account among them
existingOpt <- {
def firstHolding(ids: List[AccountId]): Future[Option[(BankAccount, Option[CallContext])]] = ids match {
case Nil => Future.successful(None)
case id :: tail =>
NewStyle.function.getBankAccount(bankId, id, callContext).flatMap { case (acc, cc) =>
if (acc.accountType == "HOLDING") Future.successful(Some((acc, cc)))
else firstHolding(tail)
}
}
firstHolding(accountIds)
}
result <- existingOpt match {
case Some((acc, cc)) => Future.successful((acc, cc))
case None =>
val newAccountId = AccountId(APIUtil.generateUUID())
for {
// Create holding account with same currency and zero balance
(holding, cc1) <- NewStyle.function.createBankAccount(
bankId = bankId,
accountId = newAccountId,
accountType = "HOLDING",
accountLabel = s"Holding account for ${parentAccount.accountId.value}",
currency = parentAccount.currency,
initialBalance = BigDecimal(0),
accountHolderName = Option(parentAccount.accountHolder).getOrElse(""),
branchId = parentAccount.branchId,
accountRoutings= Nil,
callContext = callContext
)
// Link attributes on holding account
_ <- NewStyle.function.createOrUpdateAccountAttribute(
bankId, holding.accountId, ProductCode("HOLDING"),
None, "ACCOUNT_ROLE", AccountAttributeType.STRING, "HOLDING", None, cc1
)
_ <- NewStyle.function.createOrUpdateAccountAttribute(
bankId, holding.accountId, ProductCode("HOLDING"),
None, "PARENT_ACCOUNT_ID", AccountAttributeType.STRING, parentAccount.accountId.value, None, cc1
)
// Optional reverse link on parent account
_ <- NewStyle.function.createOrUpdateAccountAttribute(
bankId, parentAccount.accountId, ProductCode(parentAccount.accountType),
None, "HOLDING_ACCOUNT_ID", AccountAttributeType.STRING, holding.accountId.value, None, cc1
)
} yield (holding, cc1)
}
} yield result
}
}

View File

@ -102,6 +102,7 @@ object TransactionRequestTypes extends OBPEnumeration[TransactionRequestTypes]{
object SEPA extends Value
object FREE_FORM extends Value
object SIMPLE extends Value
object HOLD extends Value
object CARD extends Value
object TRANSFER_TO_PHONE extends Value
object TRANSFER_TO_ATM extends Value