mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:27:01 +00:00
add DynamicEntity feature, corresponding doc not finished.
This commit is contained in:
parent
fc67401a26
commit
18c3ae9c02
@ -371,6 +371,13 @@
|
||||
<version>2.9.8</version>
|
||||
</dependency>
|
||||
|
||||
<!-- convert word to plural -->
|
||||
<dependency>
|
||||
<groupId>org.atteo</groupId>
|
||||
<artifactId>evo-inflector</artifactId>
|
||||
<version>1.2.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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."
|
||||
|
||||
///////////
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.")
|
||||
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
224
obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala
Normal file
224
obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user