diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index 557ae450d..4839c6b02 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -371,6 +371,13 @@
2.9.8
+
+
+ org.atteo
+ evo-inflector
+ 1.2.2
+
+
diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template
index 5328d33f0..c2035f20b 100644
--- a/obp-api/src/main/resources/props/sample.props.template
+++ b/obp-api/src/main/resources/props/sample.props.template
@@ -39,6 +39,8 @@ connector=mapped
#methodRouting.cache.ttl.seconds=30
## webui props cache time-to-live in seconds
#webui.props.cache.ttl.seconds=20
+## DynamicEntity cache time-to-live in seconds
+#dynamicEntity.cache.ttl.seconds=20
## enable logging all the database queries in log file
#logging.database.queries.enable=true
diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
index d96e4b32f..041f578a9 100644
--- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
+++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -54,6 +54,7 @@ import code.customer.internalMapping.MappedCustomerIDMapping
import code.customer.{MappedCustomer, MappedCustomerMessage}
import code.customeraddress.MappedCustomerAddress
import code.database.authorisation.Authorisation
+import code.dynamicEntity.DynamicEntity
import code.entitlement.MappedEntitlement
import code.entitlementrequest.MappedEntitlementRequest
import code.fx.{MappedCurrency, MappedFXRate}
@@ -655,5 +656,6 @@ object ToSchemify {
MethodRouting,
WebUiProps,
Authorisation,
+ DynamicEntity,
)++ APIBuilder_Connector.allAPIBuilderModels
}
diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala
index 89ab23f05..ce82e2061 100644
--- a/obp-api/src/main/scala/code/api/util/ApiRole.scala
+++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala
@@ -351,6 +351,18 @@ object ApiRole {
case class CanDeleteWebUiProps(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteWebUiProps = CanDeleteWebUiProps()
+ case class CanGetDynamicEntities(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canGetDynamicEntities = CanGetDynamicEntities()
+
+ case class CanCreateDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canCreateDynamicEntity = CanCreateDynamicEntity()
+
+ case class CanUpdateDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canUpdateDynamicEntity = CanUpdateDynamicEntity()
+
+ case class CanDeleteDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canDeleteDynamicEntity = CanDeleteDynamicEntity()
+
private val roles =
canSearchAllTransactions ::
canSearchAllAccounts ::
@@ -390,7 +402,7 @@ object ApiRole {
canUpdateCardsForBank ::
canGetCardsForBank ::
canCreateBranch ::
- canCreateBranchAtAnyBank ::
+ canCreateBranchAtAnyBank ::
canUpdateBranch ::
canCreateAtm ::
canCreateAtmAtAnyBank ::
@@ -454,12 +466,16 @@ object ApiRole {
canGetMethodRoutings ::
canCreateMethodRouting ::
canUpdateMethodRouting ::
- canDeleteMethodRouting ::
+ canDeleteMethodRouting ::
canUpdateCustomerNumber ::
canCreateHistoricalTransaction ::
canGetWebUiProps ::
canCreateWebUiProps ::
canDeleteWebUiProps ::
+ canGetDynamicEntities ::
+ canCreateDynamicEntity ::
+ canUpdateDynamicEntity ::
+ canDeleteDynamicEntity ::
Nil
lazy val rolesMappedToClasses = roles.map(_.getClass)
diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala
index 92398634c..138d2689e 100644
--- a/obp-api/src/main/scala/code/api/util/ApiTag.scala
+++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala
@@ -59,6 +59,7 @@ object ApiTag {
val apiTagConsent = ResourceDocTag("Consent")
val apiTagMethodRouting = ResourceDocTag("Method-Routing")
val apiTagWebUiProps = ResourceDocTag("WebUi-Props")
+ val apiTagDynamicEntity= ResourceDocTag("Dynamic-Entity")
// To mark the Berlin Group APIs suggested order of implementation
val apiTagBerlinGroupM = ResourceDocTag("Berlin-Group-M")
diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
index 253cfda8f..c69c5f2f2 100644
--- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
+++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
@@ -402,6 +402,9 @@ object ErrorMessages {
// WebUiProps Exceptions (OBP-8XXXX)
val InvalidWebUiProps = "OBP-80001: Incorrect format of name."
+ // DynamicEntity Exceptions (OBP-9XXXX)
+ val DynamicEntityNotFoundByDynamicEntityId = "OBP-90001: DynamicEntity not found. Please specify a valid value for dynamic_entity_id."
+
///////////
diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala
index 12db6fa22..4a9df1d15 100644
--- a/obp-api/src/main/scala/code/api/util/NewStyle.scala
+++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala
@@ -16,6 +16,7 @@ import code.bankconnectors.Connector
import code.branches.Branches.{Branch, DriveUpString, LobbyString}
import code.consumer.Consumers
import code.context.UserAuthContextUpdate
+import code.dynamicEntity.{DynamicEntityProvider, DynamicEntityT}
import code.entitlement.Entitlement
import code.entitlementrequest.EntitlementRequest
import code.fx.{FXRate, MappedFXRate, fx}
@@ -84,6 +85,7 @@ object NewStyle {
object function {
+
import scala.concurrent.ExecutionContext.Implicits.global
def getBranch(bankId : BankId, branchId : BranchId, callContext: Option[CallContext]): OBPReturnType[BranchT] = {
@@ -1395,6 +1397,35 @@ object NewStyle {
}
}
}
+
+ def createOrUpdateDynamicEntity(dynamicEntity: DynamicEntityT): Future[Box[DynamicEntityT]] = Future {
+ DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity)
+ }
+
+ def deleteDynamicEntity(dynamicEntityId: String): Future[Box[Boolean]] = Future {
+ DynamicEntityProvider.connectorMethodProvider.vend.delete(dynamicEntityId)
+ }
+
+ def getDynamicEntityById(dynamicEntityId : String, callContext: Option[CallContext]): OBPReturnType[DynamicEntityT] = {
+ val dynamicEntityBox: Box[DynamicEntityT] = DynamicEntityProvider.connectorMethodProvider.vend.getById(dynamicEntityId)
+ val dynamicEntity = unboxFullOrFail(dynamicEntityBox, callContext, DynamicEntityNotFoundByDynamicEntityId)
+ Future{
+ (dynamicEntity, callContext)
+ }
+ }
+
+ private[this] val dynamicEntityTTL = APIUtil.getPropsValue(s"dynamicEntity.cache.ttl.seconds", "0").toInt
+
+ def getDynamicEntities(): List[DynamicEntityT] = {
+ import scala.concurrent.duration._
+
+ var cacheKey = (randomUUID().toString, randomUUID().toString, randomUUID().toString)
+ CacheKeyFromArguments.buildCacheKey {
+ Caching.memoizeSyncWithProvider(Some(cacheKey.toString()))(dynamicEntityTTL second) {
+ DynamicEntityProvider.connectorMethodProvider.vend.getDynamicEntities()
+ }
+ }
+ }
def makeHistoricalPayment(
fromAccount: BankAccount,
diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
index 461ec1a27..9a933ef62 100644
--- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
+++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
@@ -3,13 +3,15 @@ package code.api.v4_0_0
import code.api.ChargePolicy
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.util.APIUtil._
-import code.api.util.ApiRole.canCreateAnyTransactionRequest
+import code.api.util.ApiRole._
import code.api.util.ApiTag._
-import code.api.util.ErrorMessages.{AccountNotFound, AllowedAttemptsUsedUp, BankNotFound, CounterpartyBeneficiaryPermit, InsufficientAuthorisationToCreateTransactionRequest, InvalidAccountIdFormat, InvalidBankIdFormat, InvalidChallengeAnswer, InvalidChallengeType, InvalidChargePolicy, InvalidISOCurrencyCode, InvalidJsonFormat, InvalidNumber, InvalidTransactionRequesChallengeId, InvalidTransactionRequestCurrency, InvalidTransactionRequestType, NotPositiveAmount, TransactionDisabled, TransactionRequestStatusNotInitiated, TransactionRequestTypeHasChanged, UnknownError, UserNoPermissionAccessView, UserNotLoggedIn, ViewNotFound}
+import code.api.util.ErrorMessages.{AccountNotFound, AllowedAttemptsUsedUp, BankNotFound, CounterpartyBeneficiaryPermit, InsufficientAuthorisationToCreateTransactionRequest, InvalidAccountIdFormat, InvalidBankIdFormat, InvalidChallengeAnswer, InvalidChallengeType, InvalidChargePolicy, InvalidISOCurrencyCode, InvalidJsonFormat, InvalidNumber, InvalidTransactionRequesChallengeId, InvalidTransactionRequestCurrency, InvalidTransactionRequestType, NotPositiveAmount, TransactionDisabled, TransactionRequestStatusNotInitiated, TransactionRequestTypeHasChanged, UnknownError, UserHasMissingRoles, UserNoPermissionAccessView, UserNotLoggedIn, ViewNotFound}
import code.api.util.NewStyle.HttpCode
import code.api.util._
import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJsonV140}
import code.api.v2_1_0._
+import code.api.v3_1_0.ListResult
+import code.dynamicEntity.DynamicEntityCommons
import code.model.toUserExtended
import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._
import code.transactionrequests.TransactionRequests.TransactionRequestTypes
@@ -17,15 +19,17 @@ import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{app
import code.util.Helper
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model._
-import net.liftweb.common.Full
+import net.liftweb.common.{Box, Full}
import net.liftweb.http.rest.RestHelper
import net.liftweb.json.Serialization.write
import net.liftweb.json._
-import net.liftweb.util.Props
+import net.liftweb.util.StringHelpers
+import org.atteo.evo.inflector.English
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
trait APIMethods400 {
self: RestHelper =>
@@ -60,76 +64,74 @@ trait APIMethods400 {
List(UnknownError),
Catalogs(Core, PSD2, OBWG),
apiTagBank :: apiTagPSD2AIS :: apiTagNewStyle :: Nil)
-
- lazy val getBanks : OBPEndpoint = {
+
+ lazy val getBanks: OBPEndpoint = {
case "banks" :: Nil JsonGet _ => {
cc =>
for {
(_, callContext) <- anonymousAccess(cc)
(banks, callContext) <- NewStyle.function.getBanks(callContext)
- } yield{
+ } yield {
(JSONFactory400.createBanksJson(banks), HttpCode.`200`(callContext))
}
-
+
}
}
-
- val exchangeRates =
+
+ val exchangeRates =
APIUtil.getPropsValue("webui_api_explorer_url", "") +
- "/more?version=OBPv4.0.0&list-all-banks=false&core=&psd2=&obwg=#OBPv2_2_0-getCurrentFxRate"
+ "/more?version=OBPv4.0.0&list-all-banks=false&core=&psd2=&obwg=#OBPv2_2_0-getCurrentFxRate"
// This text is used in the various Create Transaction Request resource docs
val transactionRequestGeneralText =
s"""Initiate a Payment via creating a Transaction Request.
|
- |In OBP, a `transaction request` may or may not result in a `transaction`. However, a `transaction` only has one possible state: completed.
+ |In OBP, a `transaction request` may or may not result in a `transaction`. However, a `transaction` only has one possible state: completed.
|
- |A `Transaction Request` can have one of several states.
+ |A `Transaction Request` can have one of several states.
|
- |`Transactions` are modeled on items in a bank statement that represent the movement of money.
+ |`Transactions` are modeled on items in a bank statement that represent the movement of money.
|
- |`Transaction Requests` are requests to move money which may or may not succeeed and thus result in a `Transaction`.
+ |`Transaction Requests` are requests to move money which may or may not succeeed and thus result in a `Transaction`.
|
- |A `Transaction Request` might create a security challenge that needs to be answered before the `Transaction Request` proceeds.
+ |A `Transaction Request` might create a security challenge that needs to be answered before the `Transaction Request` proceeds.
|
- |Transaction Requests contain charge information giving the client the opportunity to proceed or not (as long as the challenge level is appropriate).
+ |Transaction Requests contain charge information giving the client the opportunity to proceed or not (as long as the challenge level is appropriate).
|
- |Transaction Requests can have one of several Transaction Request Types which expect different bodies. The escaped body is returned in the details key of the GET response.
+ |Transaction Requests can have one of several Transaction Request Types which expect different bodies. The escaped body is returned in the details key of the GET response.
|This provides some commonality and one URL for many different payment or transfer types with enough flexibility to validate them differently.
|
- |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL.
+ |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL.
|
- |In sandbox mode, TRANSACTION_REQUEST_TYPE is commonly set to ACCOUNT. See getTransactionRequestTypesSupportedByBank for all supported types.
+ |In sandbox mode, TRANSACTION_REQUEST_TYPE is commonly set to ACCOUNT. See getTransactionRequestTypesSupportedByBank for all supported types.
|
- |In sandbox mode, if the amount is less than 1000 EUR (any currency, unless it is set differently on this server), the transaction request will create a transaction without a challenge, else the Transaction Request will be set to INITIALISED and a challenge will need to be answered.
+ |In sandbox mode, if the amount is less than 1000 EUR (any currency, unless it is set differently on this server), the transaction request will create a transaction without a challenge, else the Transaction Request will be set to INITIALISED and a challenge will need to be answered.
|
- |If a challenge is created you must answer it using Answer Transaction Request Challenge before the Transaction is created.
+ |If a challenge is created you must answer it using Answer Transaction Request Challenge before the Transaction is created.
|
- |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account.
+ |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account.
|
- |The following static FX rates are available in sandbox mode:
+ |The following static FX rates are available in sandbox mode:
|
- |${exchangeRates}
+ |${exchangeRates}
|
- |
- |Transaction Requests satisfy PSD2 requirements thus:
|
- |1) A transaction can be initiated by a third party application.
+ |Transaction Requests satisfy PSD2 requirements thus:
|
- |2) The customer is informed of the charge that will incurred.
+ |1) A transaction can be initiated by a third party application.
|
- |3) The call supports delegated authentication (OAuth)
+ |2) The customer is informed of the charge that will incurred.
|
- |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow.
+ |3) The call supports delegated authentication (OAuth)
|
- |There is further documentation [here](https://github.com/OpenBankProject/OBP-API/wiki/Transaction-Requests)
+ |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow.
|
- |${authenticationRequiredMessage(true)}
+ |There is further documentation [here](https://github.com/OpenBankProject/OBP-API/wiki/Transaction-Requests)
|
- |"""
-
-
+ |${authenticationRequiredMessage(true)}
+ |
+ |"""
// ACCOUNT. (we no longer create a resource doc for the general case)
@@ -170,7 +172,7 @@ trait APIMethods400 {
),
Catalogs(Core, PSD2, OBWG),
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagNewStyle))
-
+
// ACCOUNT_OTP. (we no longer create a resource doc for the general case)
resourceDocs += ResourceDoc(
createTransactionRequestAccountOtp,
@@ -252,7 +254,7 @@ trait APIMethods400 {
List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagNewStyle))
- val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50")
+ val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50")
val sharedChargePolicy = ChargePolicy.withName("SHARED")
// Transaction Request (SEPA)
@@ -334,8 +336,6 @@ trait APIMethods400 {
Some(List(canCreateAnyTransactionRequest)))
-
-
// Different Transaction Request approaches:
lazy val createTransactionRequestAccount = createTransactionRequest
lazy val createTransactionRequestAccountOtp = createTransactionRequest
@@ -351,14 +351,18 @@ trait APIMethods400 {
for {
(Full(u), callContext) <- authorizedAccess(cc)
_ <- NewStyle.function.isEnabledTransactionRequests()
- _ <- Helper.booleanToFuture(InvalidAccountIdFormat) {isValidID(accountId.value)}
- _ <- Helper.booleanToFuture(InvalidBankIdFormat) {isValidID(bankId.value)}
+ _ <- Helper.booleanToFuture(InvalidAccountIdFormat) {
+ isValidID(accountId.value)
+ }
+ _ <- Helper.booleanToFuture(InvalidBankIdFormat) {
+ isValidID(bankId.value)
+ }
(_, callContext) <- NewStyle.function.getBank(bankId, callContext)
(fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, accountId, callContext)
_ <- NewStyle.function.view(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), callContext)
_ <- Helper.booleanToFuture(InsufficientAuthorisationToCreateTransactionRequest) {
- u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId,fromAccount.accountId)) == true ||
+ u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId, fromAccount.accountId)) == true ||
hasEntitlement(fromAccount.bankId.value, u.userId, ApiRole.canCreateAnyTransactionRequest) == true
}
@@ -393,7 +397,7 @@ trait APIMethods400 {
transDetailsJson.value.currency == fromAccount.currency
}
- (createdTransactionRequest,callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match {
+ (createdTransactionRequest, callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match {
case ACCOUNT | SANDBOX_TAN => {
for {
transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) {
@@ -404,7 +408,9 @@ trait APIMethods400 {
toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
- transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))}
+ transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
+ write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))
+ }
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u,
viewId,
@@ -429,7 +435,9 @@ trait APIMethods400 {
toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id)
(toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext)
- transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))}
+ transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
+ write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))
+ }
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u,
viewId,
@@ -453,7 +461,7 @@ trait APIMethods400 {
toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id
(toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext)
toAccount <- NewStyle.function.toBankAccount(toCounterparty, callContext)
- // Check we can send money to it.
+ // Check we can send money to it.
_ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit") {
toCounterparty.isBeneficiary == true
}
@@ -461,7 +469,9 @@ trait APIMethods400 {
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy") {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
- transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints))}
+ transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
+ write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints))
+ }
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u,
viewId,
fromAccount,
@@ -492,7 +502,9 @@ trait APIMethods400 {
_ <- Helper.booleanToFuture(s"$InvalidChargePolicy") {
ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy))
}
- transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints))}
+ transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
+ write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints))
+ }
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u,
viewId,
fromAccount,
@@ -513,7 +525,9 @@ trait APIMethods400 {
}
// Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge'
transactionRequestAccountJSON = TransactionRequestAccountJsonV140(fromAccount.bankId.value, fromAccount.accountId.value)
- transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints))}
+ transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) {
+ write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints))
+ }
(createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u,
viewId,
fromAccount,
@@ -547,11 +561,11 @@ trait APIMethods400 {
|
|This endpoint totally depends on createTransactionRequest, it need get the following data from createTransactionRequest response body.
|
- |1)`TRANSACTION_REQUEST_TYPE` : is the same as createTransactionRequest request URL .
+ |1)`TRANSACTION_REQUEST_TYPE` : is the same as createTransactionRequest request URL .
|
|2)`TRANSACTION_REQUEST_ID` : is the `id` field in createTransactionRequest response body.
|
- |3) `id` : is `challenge.id` field in createTransactionRequest response body.
+ |3) `id` : is `challenge.id` field in createTransactionRequest response body.
|
|4) `answer` : must be `123`. if it is in sandbox mode. If it kafka mode, the answer can be got by phone message or other security ways.
|
@@ -583,8 +597,12 @@ trait APIMethods400 {
// Check we have a User
(Full(u), callContext) <- authorizedAccess(cc)
_ <- NewStyle.function.isEnabledTransactionRequests()
- _ <- Helper.booleanToFuture(InvalidAccountIdFormat) {isValidID(accountId.value)}
- _ <- Helper.booleanToFuture(InvalidBankIdFormat) {isValidID(bankId.value)}
+ _ <- Helper.booleanToFuture(InvalidAccountIdFormat) {
+ isValidID(accountId.value)
+ }
+ _ <- Helper.booleanToFuture(InvalidBankIdFormat) {
+ isValidID(bankId.value)
+ }
challengeAnswerJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $ChallengeAnswerJSON ", 400, callContext) {
json.extract[ChallengeAnswerJSON]
}
@@ -594,7 +612,7 @@ trait APIMethods400 {
_ <- NewStyle.function.view(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), callContext)
_ <- Helper.booleanToFuture(InsufficientAuthorisationToCreateTransactionRequest) {
- u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId,fromAccount.accountId)) == true ||
+ u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId, fromAccount.accountId)) == true ||
hasEntitlement(fromAccount.bankId.value, u.userId, ApiRole.canCreateAnyTransactionRequest) == true
}
@@ -627,7 +645,7 @@ trait APIMethods400 {
List(
OTP_VIA_API.toString,
OTP_VIA_WEB_FORM.toString
- ).exists(_ == existingTransactionRequest.challenge.challenge_type)
+ ).exists(_ == existingTransactionRequest.challenge.challenge_type)
}
challengeAnswerOBP <- NewStyle.function.validateChallengeAnswerInOBPSide(challengeAnswerJson.id, challengeAnswerJson.answer, callContext)
@@ -644,7 +662,7 @@ trait APIMethods400 {
// All Good, proceed with the Transaction creation...
(transactionRequest, callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match {
- case TRANSFER_TO_PHONE | TRANSFER_TO_ATM | TRANSFER_TO_ACCOUNT=>
+ case TRANSFER_TO_PHONE | TRANSFER_TO_ATM | TRANSFER_TO_ACCOUNT =>
NewStyle.function.createTransactionAfterChallengeV300(u, fromAccount, transReqId, transactionRequestType, callContext)
case _ =>
NewStyle.function.createTransactionAfterChallengeV210(fromAccount, existingTransactionRequest, callContext)
@@ -655,8 +673,361 @@ trait APIMethods400 {
}
}
}
-
+
+ resourceDocs += ResourceDoc(
+ getDynamicEntities,
+ implementedInApiVersion,
+ nameOf(getDynamicEntities),
+ "GET",
+ "/management/dynamic_entities",
+ "Get DynamicEntities",
+ s"""Get the all DynamicEntities.""",
+ emptyObjectJson,
+ ListResult(
+ "dynamic_entities",
+ (List(DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin)))
+ )
+ ,
+ List(
+ UserNotLoggedIn,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ Catalogs(notCore, notPSD2, notOBWG),
+ List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle),
+ Some(List(canGetDynamicEntities))
+ )
+
+
+ lazy val getDynamicEntities: OBPEndpoint = {
+ case "management" :: "dynamic_entities" :: Nil JsonGet req => {
+ cc =>
+ for {
+ (Full(u), callContext) <- authorizedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetDynamicEntities, callContext)
+ dynamicEntities <- Future(NewStyle.function.getDynamicEntities())
+ } yield {
+ val listCommons: List[DynamicEntityCommons] = dynamicEntities
+ (ListResult("dynamic_entities", listCommons), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ private def validateDynamicEntityJson(data: DynamicEntityCommons) = {
+ val metadataJson = net.liftweb.json.parse(data.metadataJson)
+
+ val rqs = (metadataJson \ "definitions" \ data.entityName \ "required").extract[Array[String]]
+
+ val propertiesFields = (metadataJson \ "definitions" \ data.entityName \ "properties").asInstanceOf[JObject].values
+ require(rqs.toSet.diff(propertiesFields.keySet).isEmpty)
+ propertiesFields.values.foreach(pair => {
+ val map = pair.asInstanceOf[Map[String, _]]
+ require(map("type").isInstanceOf[String])
+ require(map("example") != null)
+ })
+ }
+
+ resourceDocs += ResourceDoc(
+ createDynamicEntity,
+ implementedInApiVersion,
+ nameOf(createDynamicEntity),
+ "POST",
+ "/management/dynamic_entities",
+ "Add DynamicEntity",
+ s"""Add a DynamicEntity.
+ |
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |Explaination of Fields:
+ |
+ |* method_name is required String value
+ |* connector_name is required String value
+ |* is_bank_id_exact_match is required boolean value, if bank_id_pattern is exact bank_id value, this value is true; if bank_id_pattern is null or a regex, this value is false
+ |* bank_id_pattern is optional String value, it can be null, a exact bank_id or a regex
+ |* parameters is optional array of key value pairs. You can set some paremeters for this method
+ |
+ |note:
+ |
+ |* if bank_id_pattern is regex, special characters need to do escape, for example: bank_id_pattern = "some\\-id_pattern_\\d+"
+ |""",
+ DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin),
+ DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin, dynamicEntityId = Some("dynamic-entity-id")),
+ List(
+ UserNotLoggedIn,
+ UserHasMissingRoles,
+ InvalidJsonFormat,
+ UnknownError
+ ),
+ Catalogs(notCore, notPSD2, notOBWG),
+ List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle),
+ Some(List(canCreateDynamicEntity)))
+
+ lazy val createDynamicEntity: OBPEndpoint = {
+ case "management" :: "dynamic_entities" :: Nil JsonPost json -> _ => {
+ cc =>
+ for {
+ (Full(u), callContext) <- authorizedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateDynamicEntity, callContext)
+ failMsg = s"$InvalidJsonFormat The Json body should be the ${classOf[DynamicEntityCommons]}, and metadataJson should be the same structure as document example."
+ postedData <- NewStyle.function.tryons(failMsg, 400, callContext) {
+ val data = json.extract[DynamicEntityCommons]
+ validateDynamicEntityJson(data)
+ data
+ }
+
+ Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(postedData)
+ } yield {
+ val commonsData: DynamicEntityCommons = dynamicEntity
+ (commonsData, HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ updateDynamicEntity,
+ implementedInApiVersion,
+ nameOf(updateDynamicEntity),
+ "PUT",
+ "/management/dynamic_entities/DYNAMIC_ENTITY_ID",
+ "Update DynamicEntity",
+ s"""Update a DynamicEntity.
+ |
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |Explanations of Fields:
+ |
+ |* method_name is required String value
+ |* connector_name is required String value
+ |* is_bank_id_exact_match is required boolean value, if bank_id_pattern is exact bank_id value, this value is true; if bank_id_pattern is null or a regex, this value is false
+ |* bank_id_pattern is optional String value, it can be null, a exact bank_id or a regex
+ |* parameters is optional array of key value pairs. You can set some paremeters for this method
+ |note:
+ |
+ |* if bank_id_pattern is regex, special characters need to do escape, for example: bank_id_pattern = "some\\-id_pattern_\\d+"
+ |""",
+ DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin),
+ DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin, dynamicEntityId = Some("dynamic-entity-id")),
+ List(
+ UserNotLoggedIn,
+ UserHasMissingRoles,
+ InvalidJsonFormat,
+ UnknownError
+ ),
+ Catalogs(notCore, notPSD2, notOBWG),
+ List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle),
+ Some(List(canUpdateDynamicEntity)))
+
+ lazy val updateDynamicEntity: OBPEndpoint = {
+ case "management" :: "dynamic_entities" :: dynamicEntityId :: Nil JsonPut json -> _ => {
+ cc =>
+ for {
+ (Full(u), callContext) <- authorizedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, canUpdateDynamicEntity, callContext)
+
+ failMsg = s"$InvalidJsonFormat The Json body should be the ${classOf[DynamicEntityCommons]}, and metadataJson should be the same structure as document example."
+ putData <- NewStyle.function.tryons(failMsg, 400, callContext) {
+ val data = json.extract[DynamicEntityCommons].copy(dynamicEntityId = Some(dynamicEntityId))
+ validateDynamicEntityJson(data)
+ data
+ }
+
+ (_, _) <- NewStyle.function.getDynamicEntityById(dynamicEntityId, callContext)
+
+ Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(putData)
+ } yield {
+ val commonsData: DynamicEntityCommons = dynamicEntity
+ (commonsData, HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ deleteDynamicEntity,
+ implementedInApiVersion,
+ nameOf(deleteDynamicEntity),
+ "DELETE",
+ "/management/dynamic_entities/DYNAMIC_ENTITY_ID",
+ "Delete DynamicEntity",
+ s"""Delete a DynamicEntity specified by DYNAMIC_ENTITY_ID.
+ |
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ List(
+ UserNotLoggedIn,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ Catalogs(notCore, notPSD2, notOBWG),
+ List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle),
+ Some(List(canDeleteDynamicEntity)))
+
+ lazy val deleteDynamicEntity: OBPEndpoint = {
+ case "management" :: "dynamic_entities" :: dynamicEntityId :: Nil JsonDelete _ => {
+ cc =>
+ for {
+ (Full(u), callContext) <- authorizedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteDynamicEntity, callContext)
+ deleted: Box[Boolean] <- NewStyle.function.deleteDynamicEntity(dynamicEntityId)
+ } yield {
+ (deleted, HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+
+ lazy val genericEndpoint: OBPEndpoint = {
+ case EntityName(entityName) :: Nil JsonGet req => {
+ cc =>
+ Future {
+ import net.liftweb.json.JsonDSL._
+ val listName = StringHelpers.snakify(English.plural(entityName))
+ val resultList = MockerConnector.getAll(entityName)
+
+ val jValue: JValue = listName -> resultList
+
+ (jValue, HttpCode.`200`(Some(cc)))
+ }
+ }
+ case EntityName(entityName, id) JsonGet req => {
+ cc =>
+ Future {
+ (MockerConnector.getSingle(entityName, id), HttpCode.`200`(Some(cc)))
+ }
+ }
+ case EntityName(entityName) :: Nil JsonPost json -> _ => {
+ cc =>
+ Future {
+ (MockerConnector.persist(entityName, json.asInstanceOf[JObject]), HttpCode.`201`(Some(cc)))
+ }
+ }
+ case EntityName(entityName, id) JsonPut json -> _ => {
+ cc =>
+ Future {
+ (MockerConnector.persist(entityName, json.asInstanceOf[JObject], Some(id)), HttpCode.`200`(Some(cc)))
+ }
+ }
+ case EntityName(entityName, id) JsonDelete req => {
+ cc =>
+ Future {
+ (MockerConnector.delete(entityName, id), HttpCode.`200`(Some(cc)))
+ }
+ }
+ }
+
}
+
}
object APIMethods400 extends RestHelper with APIMethods400 {
diff --git a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala
new file mode 100644
index 000000000..5f6387bac
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala
@@ -0,0 +1,94 @@
+package code.api.v4_0_0
+
+import code.api.util.APIUtil.generateUUID
+import code.api.util.ErrorMessages.{InvalidJsonFormat, InvalidUrl}
+import code.api.util.NewStyle
+import net.liftweb.common.Box
+import net.liftweb.json._
+
+import scala.collection.immutable.{List, Nil}
+
+
+object EntityName {
+
+ def unapply(entityName: String): Option[String] = MockerConnector.definitionsMap.keySet.find(entityName ==)
+
+ def unapply(url: List[String]): Option[(String, String)] = url match {
+ case entityName :: id :: Nil => MockerConnector.definitionsMap.keySet.find(entityName ==).map((_, id))
+ case _ => None
+ }
+
+}
+
+object MockerConnector {
+
+ lazy val definitionsMap = NewStyle.function.getDynamicEntities().map(it => (it.entityName, DynamicEntityInfo(it.metadataJson, it.entityName))).toMap
+
+ val persistedEntities = scala.collection.mutable.Map[String, (String, JObject)]()
+
+ def persist(entityName: String, requestBody: JObject, id: Option[String] = None) = {
+ val idValue = id.orElse(Some(generateUUID()))
+ val entityToPersist = this.definitionsMap(entityName).toReponse(requestBody, id)
+ val haveIdEntity = (entityToPersist \ "id") match {
+ case JNothing => JObject(JField("id", JString(idValue.get)) :: entityToPersist.obj)
+ case _ => entityToPersist
+ }
+ persistedEntities.put(idValue.get, (entityName, haveIdEntity))
+ haveIdEntity
+ }
+
+ def getSingle(entityName: String, id: String) = {
+ persistedEntities.find(pair => pair._1 == id && pair._2._1 == entityName).map(_._2._2).getOrElse(throw new RuntimeException(s"$InvalidUrl not exists entity of id = $id"))
+ }
+
+ def getAll(entityName: String) = persistedEntities.values.filter(_._1 == entityName).map(_._2)
+
+ def delete(entityName: String, id: String): Box[Boolean] = persistedEntities.exists(it => it._1 == id && it._2._1 == entityName) match {
+ case true => persistedEntities.remove(id).map(_ => true)
+ case false => Some(false)
+ }
+}
+case class DynamicEntityInfo(defination: String, entityName: String) {
+
+ import net.liftweb.json
+
+ val subEntities: List[DynamicEntityInfo] = Nil
+
+ val jsonTypeMap = Map[String, Class[_]](
+ ("boolean", classOf[JBool]),
+ ("string", classOf[JString]),
+ ("array", classOf[JArray]),
+ ("integer", classOf[JInt]),
+ ("number", classOf[JDouble]),
+ )
+
+ val definationJson = json.parse(defination).asInstanceOf[JObject]
+ val entity = (definationJson \ "definitions" \ entityName).asInstanceOf[JObject]
+
+ def toReponse(result: JObject, id: Option[String]): JObject = {
+
+ val fieldNameToTypeName: Map[String, String] = (entity \ "properties")
+ .asInstanceOf[JObject]
+ .obj
+ .map(field => (field.name, (field.value \ "type").asInstanceOf[JString].s))
+ .toMap
+
+ val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName
+ .mapValues(jsonTypeMap(_))
+
+ val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet
+
+ val fields = result.obj.filter(it => fieldNameToType.keySet.contains(it.name))
+
+ def check(v: Boolean, msg: String) = if (!v) throw new RuntimeException(msg)
+ // if there are field type are not match the definitions, there must be bug.
+ fields.foreach(it => check(fieldNameToType(it.name).isInstance(it.value), s"""$InvalidJsonFormat "${it.name}" required type is "${fieldNameToTypeName(it.name)}"."""))
+ // if there are required field not presented, must be some bug.
+ requiredFieldNames.foreach(it => check(fields.exists(_.name == it), s"""$InvalidJsonFormat required field "$it" not presented."""))
+
+ (id, fields.exists(_.name == "id")) match {
+ case (Some(idValue), false) => JObject(JField("id", JString(idValue)) :: fields)
+ case _ => JObject(fields)
+ }
+ }
+}
\ No newline at end of file
diff --git a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala
index 7be4e2ddd..654f8f56a 100644
--- a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala
+++ b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala
@@ -29,7 +29,7 @@ package code.api.v4_0_0
import code.api.OBPRestHelper
import code.api.util.APIUtil.{OBPEndpoint, ResourceDoc, getAllowedEndpoints}
import code.api.util.{ApiVersion, VersionedOBPApis}
-import code.api.v1_3_0.{APIMethods130}
+import code.api.v1_3_0.APIMethods130
import code.api.v1_4_0.APIMethods140
import code.api.v2_0_0.APIMethods200
import code.api.v2_1_0.APIMethods210
@@ -385,7 +385,11 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w
val endpointsOf4_0_0 =
Implementations4_0_0.getBanks ::
Implementations4_0_0.createTransactionRequest ::
- Implementations4_0_0.answerTransactionRequestChallenge ::
+ Implementations4_0_0.answerTransactionRequestChallenge ::
+ Implementations4_0_0.getDynamicEntities ::
+ Implementations4_0_0.createDynamicEntity ::
+ Implementations4_0_0.updateDynamicEntity ::
+ Implementations4_0_0.deleteDynamicEntity ::
Nil
val allResourceDocs = Implementations4_0_0.resourceDocs ++
@@ -398,7 +402,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w
Implementations1_4_0.resourceDocs ++
Implementations1_3_0.resourceDocs ++
Implementations1_2_1.resourceDocs
-
+
def findResourceDoc(pf: OBPEndpoint): Option[ResourceDoc] = {
allResourceDocs.find(_.partialFunction==pf)
}
@@ -422,7 +426,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w
routes.foreach(route => {
oauthServe(apiPrefix{route}, findResourceDoc(route))
})
-
+ oauthServe(apiPrefix{Implementations4_0_0.genericEndpoint}, None)
logger.info(s"version $version has been run! There are ${routes.length} routes.")
}
diff --git a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala
new file mode 100644
index 000000000..3cde2e04a
--- /dev/null
+++ b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala
@@ -0,0 +1,42 @@
+package code.dynamicEntity
+
+import com.openbankproject.commons.model.{Converter, JsonFieldReName}
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+object DynamicEntityProvider extends SimpleInjector {
+
+ val connectorMethodProvider = new Inject(buildOne _) {}
+
+ def buildOne: MappedDynamicEntityProvider.type = MappedDynamicEntityProvider
+}
+
+trait DynamicEntityT {
+ def dynamicEntityId: Option[String]
+ def entityName: String
+ def metadataJson: String
+}
+
+case class DynamicEntityCommons(entityName: String,
+ metadataJson: String,
+ dynamicEntityId: Option[String] = None,
+ ) extends DynamicEntityT with JsonFieldReName
+
+object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommons]
+
+
+trait DynamicEntityProvider {
+ def getById(dynamicEntityId: String): Box[DynamicEntityT]
+
+ def getDynamicEntities(): List[DynamicEntityT]
+
+ def createOrUpdate(dynamicEntity: DynamicEntityT): Box[DynamicEntityT]
+
+ def delete(dynamicEntityId: String):Box[Boolean]
+}
+
+
+
+
+
+
diff --git a/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala
new file mode 100644
index 000000000..4ac2a8baa
--- /dev/null
+++ b/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala
@@ -0,0 +1,64 @@
+package code.dynamicEntity
+
+import code.api.util.CustomJsonFormats
+import code.util.MappedUUID
+import net.liftweb.common.{Box, Empty, EmptyBox, Full}
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers.tryo
+import org.apache.commons.lang3.StringUtils
+
+object MappedDynamicEntityProvider extends DynamicEntityProvider with CustomJsonFormats{
+
+ override def getById(dynamicEntityId: String): Box[DynamicEntityT] = DynamicEntity.find(
+ By(DynamicEntity.DynamicEntityId, dynamicEntityId)
+ )
+
+ override def getDynamicEntities(): List[DynamicEntity] = {
+ DynamicEntity.findAll()
+ }
+
+ override def createOrUpdate(dynamicEntity: DynamicEntityT): Box[DynamicEntityT] = {
+
+ //to find exists dynamicEntity, if dynamicEntityId supplied, query by dynamicEntityId, or use entityName and dynamicEntityId to do query
+ val existsDynamicEntity: Box[DynamicEntity] = dynamicEntity.dynamicEntityId match {
+ case Some(id) if (StringUtils.isNotBlank(id)) => getByDynamicEntityId(id)
+ case _ => Empty
+ }
+ val entityToPersist = existsDynamicEntity match {
+ case _: EmptyBox => DynamicEntity.create
+ case Full(dynamicEntity) => dynamicEntity
+ }
+
+ tryo{
+ entityToPersist
+ .EntityName(dynamicEntity.entityName)
+ .MetadataJson(dynamicEntity.metadataJson)
+ .saveMe()
+ }
+ }
+
+
+ override def delete(dynamicEntityId: String): Box[Boolean] = getByDynamicEntityId(dynamicEntityId).map(_.delete_!)
+
+ private[this] def getByDynamicEntityId(dynamicEntityId: String): Box[DynamicEntity] = DynamicEntity.find(By(DynamicEntity.DynamicEntityId, dynamicEntityId))
+
+}
+
+class DynamicEntity extends DynamicEntityT with LongKeyedMapper[DynamicEntity] with IdPK with CustomJsonFormats{
+
+ override def getSingleton = DynamicEntity
+
+ object DynamicEntityId extends MappedUUID(this)
+ object EntityName extends MappedString(this, 255)
+
+ object MetadataJson extends MappedText(this)
+
+ override def dynamicEntityId: Option[String] = Option(DynamicEntityId.get)
+ override def entityName: String = EntityName.get
+ override def metadataJson: String = MetadataJson.get
+}
+
+object DynamicEntity extends DynamicEntity with LongKeyedMetaMapper[DynamicEntity] {
+ override def dbIndexes = UniqueIndex(DynamicEntityId) :: UniqueIndex(MetadataJson) :: super.dbIndexes
+}
+
diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala
new file mode 100644
index 000000000..917ca1d83
--- /dev/null
+++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala
@@ -0,0 +1,224 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2018, TESOBE Ltd
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+Email: contact@tesobe.com
+TESOBE Ltd
+Osloerstrasse 16/17
+Berlin 13359, Germany
+
+This product includes software developed at
+TESOBE (http://www.tesobe.com/)
+ */
+package code.api.v4_0_0
+
+import code.api.ErrorMessage
+import code.api.util.ApiRole._
+import code.api.util.ApiVersion
+import code.api.util.ErrorMessages._
+import code.api.v4_0_0.APIMethods400.Implementations4_0_0
+import code.dynamicEntity.DynamicEntityCommons
+import code.entitlement.Entitlement
+import com.github.dwickern.macros.NameOf.nameOf
+import net.liftweb.json.Serialization.write
+import org.scalatest.Tag
+import code.api.util.APIUtil.OAuth._
+import scala.collection.immutable.List
+
+class DynamicEntityTest extends V400ServerSetup {
+
+ /**
+ * Test tags
+ * Example: To run tests with tag "getPermissions":
+ * mvn test -D tagsToInclude
+ *
+ * This is made possible by the scalatest maven plugin
+ */
+ object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString)
+ object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.createDynamicEntity))
+ object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.updateDynamicEntity))
+ object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getDynamicEntities))
+ object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.deleteDynamicEntity))
+
+ val rightEntity = DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin)
+ // wrong metadataJson
+ val wrongEntity = DynamicEntityCommons(entityName = "FooBar", metadataJson =
+ """
+ |{
+ | "definitions": {
+ | "FooBar": {
+ | "required": [
+ | "name"
+ | ],
+ | "properties": {
+ | "name_wrong": {
+ | "type": "string",
+ | "example": "James Brown"
+ | },
+ | "number": {
+ | "type": "integer",
+ | "example": "698761728934"
+ | }
+ | }
+ | }
+ | }
+ |}
+ |""".stripMargin)
+
+
+ feature("Add a DynamicEntity v4.0.4- Unauthorized access") {
+ scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
+ When("We make a request v4.0.0")
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST
+ val response400 = makePostRequest(request400, write(rightEntity))
+ Then("We should get a 400")
+ response400.code should equal(400)
+ And("error should be " + UserNotLoggedIn)
+ response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn)
+ }
+ }
+ feature("Update a DynamicEntity v4.0.4- Unauthorized access") {
+ scenario("We will call the endpoint without user credentials", ApiEndpoint2, VersionOfApi) {
+ When("We make a request v4.0.0")
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities"/ "some-method-routing-id").PUT
+ val response400 = makePutRequest(request400, write(rightEntity))
+ Then("We should get a 400")
+ response400.code should equal(400)
+ And("error should be " + UserNotLoggedIn)
+ response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn)
+ }
+ }
+ feature("Get DynamicEntities v4.0.4- Unauthorized access") {
+ scenario("We will call the endpoint without user credentials", ApiEndpoint3, VersionOfApi) {
+ When("We make a request v4.0.0")
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities").GET
+ val response400 = makeGetRequest(request400)
+ Then("We should get a 400")
+ response400.code should equal(400)
+ And("error should be " + UserNotLoggedIn)
+ response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn)
+ }
+ }
+ feature("Delete the DynamicEntity specified by METHOD_ROUTING_ID v4.0.4- Unauthorized access") {
+ scenario("We will call the endpoint without user credentials", ApiEndpoint4, VersionOfApi) {
+ When("We make a request v4.0.0")
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / "METHOD_ROUTING_ID").DELETE
+ val response400 = makeDeleteRequest(request400)
+ Then("We should get a 400")
+ response400.code should equal(400)
+ And("error should be " + UserNotLoggedIn)
+ response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn)
+ }
+ }
+
+
+ feature("Add a DynamicEntity v4.0.4- Unauthorized access - Authorized access") {
+ scenario("We will call the endpoint without the proper Role " + canCreateDynamicEntity, ApiEndpoint1, VersionOfApi) {
+ When("We make a request v4.0.4without a Role " + canCreateTaxResidence)
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST <@(user1)
+ val response400 = makePostRequest(request400, write(rightEntity))
+ Then("We should get a 403")
+ response400.code should equal(403)
+ And("error should be " + UserHasMissingRoles + CanCreateDynamicEntity)
+ response400.body.extract[ErrorMessage].message should equal (UserHasMissingRoles + CanCreateDynamicEntity)
+ }
+
+ scenario("We will call the endpoint with the proper Role " + canCreateDynamicEntity , ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) {
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateDynamicEntity.toString)
+ When("We make a request v4.0.0")
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST <@(user1)
+ val response400 = makePostRequest(request400, write(rightEntity))
+ Then("We should get a 201")
+ response400.code should equal(201)
+ val customerJson = response400.body.extract[DynamicEntityCommons]
+
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanUpdateDynamicEntity.toString)
+ When("We make a request v4.0.4with the Role " + canUpdateDynamicEntity)
+
+ {
+ // update success
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / customerJson.dynamicEntityId.get ).PUT <@(user1)
+ val metadataJson = customerJson.metadataJson
+ val entityName = customerJson.entityName
+ val response400 = makePutRequest(request400, write(customerJson.copy(entityName = "Hello", metadataJson.replace(entityName, "Hello"))))
+ Then("We should get a 200")
+ response400.code should equal(200)
+ val dynamicEntitiesJson = response400.body.extract[DynamicEntityCommons]
+ dynamicEntitiesJson.entityName should be ("Hello")
+ }
+
+ {
+ // update a not exists DynamicEntity
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / "not-exists-id" ).PUT <@(user1)
+ val response400 = makePutRequest(request400, write(customerJson))
+ Then("We should get a 400")
+ response400.code should equal(400)
+ response400.body.extract[ErrorMessage].message should startWith (DynamicEntityNotFoundByDynamicEntityId)
+ }
+
+ {
+ // update a DynamicEntity with wrong metadataJson
+ val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / customerJson.dynamicEntityId.get ).PUT <@(user1)
+ val response400 = makePutRequest(request400, write(wrongEntity))
+ Then("We should get a 400")
+ response400.code should equal(400)
+ response400.body.extract[ErrorMessage].message should startWith (InvalidJsonFormat)
+ }
+
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetDynamicEntities.toString)
+ When("We make a request v4.0.4with the Role " + canGetDynamicEntities)
+ val requestGet400 = (v4_0_0_Request / "management" / "dynamic_entities").GET <@(user1)
+ val responseGet400 = makeGetRequest(requestGet400)
+ Then("We should get a 200")
+ responseGet400.code should equal(200)
+ val json = responseGet400.body \ "dynamic_entities"
+ val dynamicEntitiesGetJson = json.extract[List[DynamicEntityCommons]]
+
+ dynamicEntitiesGetJson.size should be (1)
+
+ Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteDynamicEntity.toString)
+ When("We make a request v4.0.4with the Role " + canDeleteDynamicEntity)
+ val requestDelete400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntitiesGetJson.head.dynamicEntityId.get).DELETE <@(user1)
+ val responseDelete400 = makeDeleteRequest(requestDelete400)
+ Then("We should get a 200")
+ responseDelete400.code should equal(200)
+
+ }
+ }
+
+
+}