diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index d63b0172d..6cde75351 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -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 diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index df9f3614f..0e55a99a1 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -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() diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 4bb912519..f1b27003c 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -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, diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 3ca749b81..581435e1a 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -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, diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala index a2b74af54..c706b9a9c 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnectorInternal.scala @@ -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 + } + } diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala index 322d70f64..206138292 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala @@ -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