diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 9f1971d32..2d77ddd92 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -589,9 +589,9 @@ apiOptions.getProductsIsPublic = true apiOptions.getTransactionTypesIsPublic = true apiOptions.getCurrentFxRateIsPublic = true -## Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID +## Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID, the default value is OBP. ## e.g. developers could use /my/accounts as well as /my/banks/BANK_ID/accounts -defaultBank.bank_id=THE_DEFAULT_BANK_ID +defaultBank.bank_id=OBP diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 3f96a4e4f..1de144150 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -137,7 +137,7 @@ import code.regulatedentities.attribute.RegulatedEntityAttribute import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.Functions.Implicits._ import com.openbankproject.commons.util.{ApiVersion, Functions} - +import code.bankaccountbalance.BankAccountBalance import javax.mail.internet.MimeMessage import net.liftweb.common._ import net.liftweb.db.{DB, DBLogEntry} @@ -559,6 +559,9 @@ class Boot extends MdcLoggable { logger.info (s"props_identifier is : ${APIUtil.getPropsValue("props_identifier", "NONE-SET")}") + // This will work for both portal and API modes. This page is used for testing if the API is running properly. + val alivePage = List( Menu.i("Alive") / "alive") + val commonMap = List(Menu.i("Home") / "index") ::: List( Menu.i("Plain") / "plain", Menu.i("Static") / "static", @@ -595,12 +598,12 @@ class Boot extends MdcLoggable { Menu.i("confirm-bg-consent-request-redirect-uri") / "confirm-bg-consent-request-redirect-uri" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-vrp-consent-request") / "confirm-vrp-consent-request" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-vrp-consent") / "confirm-vrp-consent" >> AuthUser.loginFirst //OAuth consent page - ) ++ accountCreation ++ Admin.menus + ) ++ accountCreation ++ Admin.menus++ alivePage // Build SiteMap val sitemap = APIUtil.getPropsValue("server_mode", "apis,portal") match { case mode if mode == "portal" => commonMap - case mode if mode == "apis" => List() + case mode if mode == "apis" => alivePage case mode if mode.contains("apis") && mode.contains("portal") => commonMap case _ => commonMap } @@ -1129,6 +1132,7 @@ object ToSchemify { CustomerAccountLink, TransactionIdMapping, RegulatedEntityAttribute, + BankAccountBalance ) // start grpc server diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index a37cb357d..9f417d64f 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5669,7 +5669,23 @@ object SwaggerDefinitionsJSON { lazy val regulatedEntityAttributesJsonV510 = RegulatedEntityAttributesJsonV510( List(regulatedEntityAttributeResponseJsonV510) ) + + lazy val bankAccountBalanceRequestJsonV510 = BankAccountBalanceRequestJsonV510( + balance_type = balanceTypeExample.value, + balance_amount = balanceAmountExample.value + ) + lazy val bankAccountBalanceResponseJsonV510 = BankAccountBalanceResponseJsonV510( + bank_id = bankIdExample.value, + account_id = accountIdExample.value, + balance_id = balanceIdExample.value, + balance_type = balanceTypeExample.value, + balance_amount = balanceAmountExample.value + ) + + lazy val bankAccountBalancesJsonV510 = BankAccountBalancesJsonV510( + balances = List(bankAccountBalanceResponseJsonV510) + ) //The common error or success format. //Just some helper format to use in Json case class NotSupportedYet() diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 33297ba01..07862ae15 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -1009,7 +1009,23 @@ object ApiRole extends MdcLoggable{ case class CanGetBankLevelEndpointTag(requiresBankId: Boolean = true) extends ApiRole lazy val canGetBankLevelEndpointTag = CanGetBankLevelEndpointTag() - + +// // BankAccountBalance roles + case class CanCreateBankAccountBalance(requiresBankId: Boolean = true) extends ApiRole + lazy val canCreateBankAccountBalance = CanCreateBankAccountBalance() +// +// case class CanGetBankAccountBalance(requiresBankId: Boolean = false) extends ApiRole +// lazy val canGetBankAccountBalance = CanGetBankAccountBalance() +// +// case class CanGetBankAccountBalances(requiresBankId: Boolean = false) extends ApiRole +// lazy val canGetBankAccountBalances = CanGetBankAccountBalances() + + case class CanUpdateBankAccountBalance(requiresBankId: Boolean = true) extends ApiRole + lazy val canUpdateBankAccountBalance = CanUpdateBankAccountBalance() + + case class CanDeleteBankAccountBalance(requiresBankId: Boolean = true) extends ApiRole + lazy val canDeleteBankAccountBalance = CanDeleteBankAccountBalance() + case class CanCreateHistoricalTransactionAtBank(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateHistoricalTransactionAtBank = CanCreateHistoricalTransactionAtBank() 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 8e6265150..db3934772 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -67,6 +67,7 @@ object ApiTag { val apiTagMXOpenFinance = ResourceDocTag("MXOpenFinance") val apiTagAggregateMetrics = ResourceDocTag("Aggregate-Metrics") val apiTagSystemIntegrity = ResourceDocTag("System-Integrity") + val apiTagBalance = ResourceDocTag("Balance") val apiTagWebhook = ResourceDocTag("Webhook") val apiTagMockedData = ResourceDocTag("Mocked-Data") val apiTagConsent = ResourceDocTag("Consent") 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 48afde5e0..21ed948e7 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -250,6 +250,8 @@ object ErrorMessages { val InvalidUserProvider = "OBP-20103: Invalid DAuth User Provider." val UserNotFoundByProviderAndProvideId= "OBP-20104: User not found by PROVIDER and PROVIDER_ID." + val BankAccountBalanceNotFoundById = "OBP-20105: BankAccountBalance not found. Please specify a valid value for BALANCE_ID." + // OAuth 2 val ApplicationNotIdentified = "OBP-20200: The application cannot be identified. " val Oauth2IsNotAllowed = "OBP-20201: OAuth2 is not allowed at this instance." diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 610f24125..18c385d30 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -298,6 +298,12 @@ object ExampleValue { lazy val accountTypeExample = ConnectorField("AC","A short code that represents the type of the account as provided by the bank.") lazy val balanceAmountExample = ConnectorField("50.89", "The balance on the account.") + + lazy val balanceTypeExample = ConnectorField("openingBooked", "The balance type.") + glossaryItems += makeGlossaryItem("balance_type", balanceTypeExample) + + lazy val balanceIdExample = ConnectorField("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh", "A string that MUST uniquely identify the Account Balance on this OBP instance, can be used in all cache.") + glossaryItems += makeGlossaryItem("balance_id", balanceIdExample) lazy val amountExample = ConnectorField("10.12", "The balance on the account.") diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 2a8397f79..f14837a1f 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -3520,8 +3520,11 @@ object Glossary extends MdcLoggable { } private def getListOfFiles(): List[File] = { - val glossaryPath = new File(getClass.getResource("").toURI.toString.replaceFirst("target/.*", "").replace("file:", ""), - "/src/main/resources/docs/glossary") + import java.net.URLDecoder + import java.nio.charset.StandardCharsets + val resourceUrl = getClass.getClassLoader.getResource("docs/glossary") + val resourcePath = URLDecoder.decode(resourceUrl.getPath, StandardCharsets.UTF_8.name()) + val glossaryPath = new File(resourcePath) logger.info(s"|---> Glossary path: $glossaryPath") if (glossaryPath.exists && glossaryPath.isDirectory) { @@ -3531,7 +3534,7 @@ object Glossary extends MdcLoggable { .filter(_.getName.endsWith(".md")) .toList } else { - logger.error(s"Do not have any files under glossary path ($glossaryPath), please double check the folder: obp-api/src/main/resources/docs/glossary") + logger.error(s"Do not have any files under glossary path ($glossaryPath), please double check the folder path: $glossaryPath") List.empty[File] } } diff --git a/obp-api/src/main/scala/code/api/util/newstyle/BankAccountBalanceNewStyle.scala b/obp-api/src/main/scala/code/api/util/newstyle/BankAccountBalanceNewStyle.scala new file mode 100644 index 000000000..c45360194 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/newstyle/BankAccountBalanceNewStyle.scala @@ -0,0 +1,79 @@ +package code.api.util.newstyle + +import code.api.util.APIUtil.{OBPReturnType, unboxFullOrFail} +import code.bankconnectors.Connector +import code.api.util.{APIUtil, CallContext} +import code.api.util.CallContext +import code.api.util.ErrorMessages.BankAccountBalanceNotFoundById +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.model.{AccountId, BankAccountBalanceTrait, BankId} +import com.openbankproject.commons.model.BalanceId + + +object BankAccountBalanceNewStyle { + + def getBankAccountBalances( + accountId: AccountId, + callContext: Option[CallContext] + ): OBPReturnType[List[BankAccountBalanceTrait]] = { + Connector.connector.vend.getBankAccountBalancesByAccountId( + accountId: AccountId, + callContext: Option[CallContext] + ) map { + i => (APIUtil.connectorEmptyResponse(i._1, callContext), i._2) + } + } + + def getBankAccountBalanceById( + balanceId: BalanceId, + callContext: Option[CallContext] + ): OBPReturnType[BankAccountBalanceTrait] = { + Connector.connector.vend.getBankAccountBalanceById( + balanceId: BalanceId, + callContext: Option[CallContext] + ).map { + result => + ( + unboxFullOrFail( + result._1, + result._2, + s"$BankAccountBalanceNotFoundById Current BALANCE_ID(${balanceId.value})", + 404), + callContext + ) + } + } + + def createOrUpdateBankAccountBalance( + bankId: BankId, + accountId: AccountId, + balanceId: Option[BalanceId], + balanceType: String, + balanceAmount: BigDecimal, + callContext: Option[CallContext] + ): OBPReturnType[BankAccountBalanceTrait] = { + Connector.connector.vend.createOrUpdateBankAccountBalance( + bankId: BankId, + accountId: AccountId, + balanceId: Option[BalanceId], + balanceType: String, + balanceAmount: BigDecimal, + callContext: Option[CallContext] + ) map { + i => (APIUtil.connectorEmptyResponse(i._1, callContext), i._2) + } + } + + def deleteBankAccountBalance( + balanceId: BalanceId, + callContext: Option[CallContext] + ): OBPReturnType[Boolean] = { + Connector.connector.vend.deleteBankAccountBalance( + balanceId: BalanceId, + callContext: Option[CallContext] + ) map { + i => (APIUtil.connectorEmptyResponse(i._1, callContext), i._2) + } + } + +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index d69b07510..a7885cfba 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -188,7 +188,6 @@ trait APIMethods510 { } } - staticResourceDocs += ResourceDoc( createRegulatedEntity, implementedInApiVersion, @@ -3899,7 +3898,7 @@ trait APIMethods510 { nameOf(getBankAccountsBalancesThroughView), "GET", "/banks/BANK_ID/views/VIEW_ID/balances", - "Get Account Balances by BANK_ID", + "Get Account Balances by BANK_ID through the VIEW_ID", """Get the Balances for the Account specified by BANK_ID.""", EmptyBody, accountBalancesV400Json, @@ -4840,6 +4839,228 @@ trait APIMethods510 { } } + staticResourceDocs += ResourceDoc( + createBankAccountBalance, + implementedInApiVersion, + nameOf(createBankAccountBalance), + "POST", + "/banks/BANK_ID/accounts/ACCOUNT_ID/balances", + "Create Bank Account Balance", + s"""Create a new Balance for a Bank Account. + | + |${userAuthenticationMessage(true)} + | + |""", + bankAccountBalanceRequestJsonV510, + bankAccountBalanceResponseJsonV510, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagAccount, apiTagBalance), + Some(List(canCreateBankAccountBalance)) + ) + + lazy val createBankAccountBalance: OBPEndpoint = { + case "banks" :: BankId(bankId):: "accounts" :: AccountId(accountId) :: "balances" :: Nil JsonPost json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- SS.user + postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $BankAccountBalanceRequestJsonV510 ", 400, callContext) { + json.extract[BankAccountBalanceRequestJsonV510] + } + balanceAmount <- NewStyle.function.tryons(s"$InvalidNumber Current balance_amount is ${postedData.balance_amount}" , 400, cc.callContext) { + BigDecimal(postedData.balance_amount) + } + (balance, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.createOrUpdateBankAccountBalance( + bankId = bankId, + accountId = accountId, + balanceId = None, + balanceType = postedData.balance_type, + balanceAmount = balanceAmount, + callContext = cc.callContext + ) + } yield { + (JSONFactory510.createBankAccountBalanceJson(balance), HttpCode.`201`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getBankAccountBalanceById, + implementedInApiVersion, + nameOf(getBankAccountBalanceById), + "GET", + "/banks/BANK_ID/accounts/ACCOUNT_ID/balances/BALANCE_ID", + "Get Bank Account Balance By ID", + s"""Get a specific Bank Account Balance by its BALANCE_ID. + | + |${userAuthenticationMessage(true)} + | + |""", + EmptyBody, + bankAccountBalanceResponseJsonV510, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagAccount, apiTagBalance) + ) + + lazy val getBankAccountBalanceById: OBPEndpoint = { + case "banks" :: BankId(bankId):: "accounts" :: AccountId(accountId) :: "balances" :: BalanceId(balanceId) :: Nil JsonGet _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- SS.user + (balance, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalanceById( + balanceId, + callContext + ) + } yield { + (JSONFactory510.createBankAccountBalanceJson(balance), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getAllBankAccountBalances, + implementedInApiVersion, + nameOf(getAllBankAccountBalances), + "GET", + "/banks/BANK_ID/accounts/ACCOUNT_ID/balances", + "Get All Bank Account Balances", + s"""Get all Balances for a Bank Account. + | + |${userAuthenticationMessage(true)} + | + |""", + EmptyBody, + bankAccountBalancesJsonV510, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagAccount, apiTagBalance) + ) + + lazy val getAllBankAccountBalances: OBPEndpoint = { + case "banks" :: BankId(bankId):: "accounts" :: AccountId(accountId) :: "balances" :: Nil JsonGet _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- SS.user + (balances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalances( + accountId, + callContext + ) + } yield { + (JSONFactory510.createBankAccountBalancesJson(balances), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + updateBankAccountBalance, + implementedInApiVersion, + nameOf(updateBankAccountBalance), + "PUT", + "/banks/BANK_ID/accounts/ACCOUNT_ID/balances/BALANCE_ID", + "Update Bank Account Balance", + s"""Update an existing Bank Account Balance specified by BALANCE_ID. + | + |${userAuthenticationMessage(true)} + | + |""", + bankAccountBalanceRequestJsonV510, + bankAccountBalanceResponseJsonV510, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagAccount, apiTagBalance), + Some(List(canUpdateBankAccountBalance)) + ) + + lazy val updateBankAccountBalance: OBPEndpoint = { + case "banks" :: BankId(bankId):: "accounts" :: AccountId(accountId) :: "balances" :: BalanceId(balanceId) :: Nil JsonPut json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- SS.user + postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the BankAccountBalanceRequestJsonV510 ", 400, callContext) { + json.extract[BankAccountBalanceRequestJsonV510] + } + balanceAmount <- NewStyle.function.tryons(s"$InvalidNumber Current balance_amount is ${postedData.balance_amount}" , 400, cc.callContext) { + BigDecimal(postedData.balance_amount) + } + (balance, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalanceById( + balanceId, + callContext + ) + (balance, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.createOrUpdateBankAccountBalance( + bankId = bankId, + accountId = accountId, + balanceId = Some(balanceId), + balanceType = postedData.balance_type, + balanceAmount = balanceAmount, + callContext = callContext + ) + } yield { + (JSONFactory510.createBankAccountBalanceJson(balance), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteBankAccountBalance, + implementedInApiVersion, + nameOf(deleteBankAccountBalance), + "DELETE", + "/banks/BANK_ID/accounts/ACCOUNT_ID/balances/BALANCE_ID", + "Delete Bank Account Balance", + s"""Delete a Bank Account Balance specified by BALANCE_ID. + | + |${userAuthenticationMessage(true)} + | + |""", + EmptyBody, + EmptyBody, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagAccount, apiTagBalance), + Some(List(canDeleteBankAccountBalance)) + ) + + lazy val deleteBankAccountBalance: OBPEndpoint = { + case "banks" :: BankId(bankId):: "accounts" :: AccountId(accountId) :: "balances" :: BalanceId(balanceId) :: Nil JsonDelete _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- SS.user + (balance, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalanceById( + balanceId, + callContext + ) + (deleted, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.deleteBankAccountBalance( + balanceId, + callContext + ) + } yield { + (Full(deleted), HttpCode.`204`(callContext)) + } + } + } } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 427cec8ab..80bc6c368 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -611,6 +611,24 @@ case class SyncExternalUserJson(user_id: String) case class UserValidatedJson(is_validated: Boolean) + +case class BankAccountBalanceRequestJsonV510( + balance_type: String, + balance_amount: String +) + +case class BankAccountBalanceResponseJsonV510( + bank_id: String, + account_id: String, + balance_id: String, + balance_type: String, + balance_amount: String +) + +case class BankAccountBalancesJsonV510( + balances: List[BankAccountBalanceResponseJsonV510] +) + object JSONFactory510 extends CustomJsonFormats { def createTransactionRequestJson(tr : TransactionRequest, transactionRequestAttributes: List[TransactionRequestAttributeTrait] ) : TransactionRequestJsonV510 = { @@ -651,7 +669,7 @@ object JSONFactory510 extends CustomJsonFormats { createTransactionRequestJson(transactionRequest, transactionRequestAttributes) )) } - + def createViewJson(view: View): CustomViewJsonV510 = { val alias = if (view.usePublicAliasIfOneExists) @@ -1073,7 +1091,7 @@ object JSONFactory510 extends CustomJsonFormats { logo_url = if (c.logoUrl.get == null || c.logoUrl.get.isEmpty ) null else Some(c.logoUrl.get) ) } - + def createConsumersJson(consumers:List[Consumer]) = { ConsumersJsonV510(consumers.map(createConsumerJSON(_,None))) } @@ -1100,7 +1118,7 @@ object JSONFactory510 extends CustomJsonFormats { agent_number = agent.number ))) } - + def createRegulatedEntityAttributeJson(attribute: RegulatedEntityAttributeTrait): RegulatedEntityAttributeResponseJsonV510 = { RegulatedEntityAttributeResponseJsonV510( regulated_entity_id = attribute.regulatedEntityId.value, @@ -1119,7 +1137,21 @@ object JSONFactory510 extends CustomJsonFormats { attributes.map(createRegulatedEntityAttributeJson) ) } - + + def createBankAccountBalanceJson(balance: BankAccountBalanceTrait): BankAccountBalanceResponseJsonV510 = { + BankAccountBalanceResponseJsonV510( + bank_id = balance.bankId.value, + account_id = balance.accountId.value, + balance_id = balance.balanceId.value, + balance_type = balance.balanceType, + balance_amount = balance.balanceAmount.toString + ) + } + + def createBankAccountBalancesJson(balances: List[BankAccountBalanceTrait]): BankAccountBalancesJsonV510 = { + BankAccountBalancesJsonV510( + balances.map(createBankAccountBalanceJson) + ) + } } - diff --git a/obp-api/src/main/scala/code/bankaccountbalance/BankAccountBalance.scala b/obp-api/src/main/scala/code/bankaccountbalance/BankAccountBalance.scala new file mode 100644 index 000000000..5f6e0712c --- /dev/null +++ b/obp-api/src/main/scala/code/bankaccountbalance/BankAccountBalance.scala @@ -0,0 +1,36 @@ +package code.bankaccountbalance + +import code.model.dataAccess.MappedBankAccount +import code.util.{Helper, MappedUUID} + +import com.openbankproject.commons.model.{BankId, AccountId, BalanceId, BankAccountBalanceTrait} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo + + +class BankAccountBalance extends BankAccountBalanceTrait with LongKeyedMapper[BankAccountBalance] with CreatedUpdated with IdPK { + + override def getSingleton = BankAccountBalance + + object BankId_ extends MappedUUID(this) + object AccountId_ extends MappedUUID(this) + object BalanceId_ extends MappedUUID(this) + object BalanceType extends MappedString(this, 255) + //this is the smallest unit of currency! eg. cents, yen, pence, øre, etc. + object BalanceAmount extends MappedLong(this) + + val foreignMappedBankAccountCurrency = tryo{code.model.dataAccess.MappedBankAccount + .find( + By(MappedBankAccount.theAccountId, AccountId_.get)) + .map(_.currency) + .getOrElse("EUR") + }.getOrElse("EUR") + + override def bankId: BankId = BankId(BankId_.get) + override def accountId: AccountId = AccountId(AccountId_.get) + override def balanceId: BalanceId = BalanceId(BalanceId_.get) + override def balanceType: String = BalanceType.get + override def balanceAmount: BigDecimal = Helper.smallestCurrencyUnitToBigDecimal(BalanceAmount.get, foreignMappedBankAccountCurrency) +} + +object BankAccountBalance extends BankAccountBalance with LongKeyedMetaMapper[BankAccountBalance] {} diff --git a/obp-api/src/main/scala/code/bankaccountbalance/BankAccountBalanceProvider.scala b/obp-api/src/main/scala/code/bankaccountbalance/BankAccountBalanceProvider.scala new file mode 100644 index 000000000..2568257b4 --- /dev/null +++ b/obp-api/src/main/scala/code/bankaccountbalance/BankAccountBalanceProvider.scala @@ -0,0 +1,116 @@ +package code.bankaccountbalance + +import code.model.dataAccess.MappedBankAccount +import code.util.Helper +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.model.{BankId, AccountId} +import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo +import net.liftweb.util.SimpleInjector +import com.openbankproject.commons.model.BalanceId + +import scala.concurrent.Future + +object BankAccountBalanceX extends SimpleInjector { + + val bankAccountBalanceProvider = new Inject(buildOne _) {} + + def buildOne: BankAccountBalanceProviderTrait = MappedBankAccountBalanceProvider + + // Helper to get the count out of an option + def countOfBankAccountBalance(listOpt: Option[List[BankAccountBalance]]): Int = { + val count = listOpt match { + case Some(list) => list.size + case None => 0 + } + count + } +} + +trait BankAccountBalanceProviderTrait { + + def getBankAccountBalances(accountId: AccountId): Future[Box[List[BankAccountBalance]]] + + def getBankAccountBalanceById(balanceId: BalanceId): Future[Box[BankAccountBalance]] + + def createOrUpdateBankAccountBalance( + bankId: BankId, + accountId: AccountId, + balanceId: Option[BalanceId], + balanceType: String, + balanceAmount: BigDecimal): Future[Box[BankAccountBalance]] + + def deleteBankAccountBalance(balanceId: BalanceId): Future[Box[Boolean]] + +} + +object MappedBankAccountBalanceProvider extends BankAccountBalanceProviderTrait { + + override def getBankAccountBalances(accountId: AccountId): Future[Box[List[BankAccountBalance]]] = Future { + tryo{ + BankAccountBalance.findAll( + By(BankAccountBalance.AccountId_,accountId.value) + )} + } + + override def getBankAccountBalanceById(balanceId: BalanceId): Future[Box[BankAccountBalance]] = Future { + // Find a balance by its ID + BankAccountBalance.find( + By(BankAccountBalance.BalanceId_, balanceId.value) + ) + } + + override def createOrUpdateBankAccountBalance( + bankId: BankId, + accountId: AccountId, + balanceId: Option[BalanceId], + balanceType: String, + balanceAmount: BigDecimal + ): Future[Box[BankAccountBalance]] = Future { + // Get the MappedBankAccount for the given account ID + val mappedBankAccount = code.model.dataAccess.MappedBankAccount + .find( + By(MappedBankAccount.theAccountId, accountId.value) + ) + + mappedBankAccount match { + case Full(account) => + balanceId match { + case Some(id) => + BankAccountBalance.find( + By(BankAccountBalance.BalanceId_, id.value) + ) match { + case Full(balance) => + tryo { + balance + .BankId_(bankId.value) + .AccountId_(accountId.value) + .BalanceType(balanceType) + .BalanceAmount(Helper.convertToSmallestCurrencyUnits(balanceAmount, account.currency)) + .saveMe() + } + case _ => Empty + } + case _ => + tryo { + BankAccountBalance.create + .BankId_(bankId.value) + .AccountId_(accountId.value) + .BalanceType(balanceType) + .BalanceAmount(Helper.convertToSmallestCurrencyUnits(balanceAmount, account.currency)) + .saveMe() + } + } + case _ => Empty + } + } + + override def deleteBankAccountBalance(balanceId: BalanceId): Future[Box[Boolean]] = Future { + // Delete a balance by its ID + BankAccountBalance.find( + By(BankAccountBalance.BalanceId_, balanceId.value) + ).map(_.delete_!) + } + +} diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index fdf9efe7d..d73e249de 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -1893,4 +1893,29 @@ trait Connector extends MdcLoggable { regulatedEntityId: String, callContext: Option[CallContext] ): OBPReturnType[Box[RegulatedEntityTrait]] = Future{(Failure(setUnimplementedError(nameOf(getRegulatedEntityByEntityId _))), callContext)} + + def getBankAccountBalancesByAccountId( + accountId: AccountId, + callContext: Option[CallContext] + ): OBPReturnType[Box[List[BankAccountBalanceTrait]]] = Future{(Failure(setUnimplementedError(nameOf(getBankAccountBalancesByAccountId(_, _)))), callContext)} + + def getBankAccountBalanceById( + balanceId: BalanceId, + callContext: Option[CallContext] + ): OBPReturnType[Box[BankAccountBalanceTrait]] = Future{(Failure(setUnimplementedError(nameOf(getBankAccountBalanceById _))), callContext)} + + + def createOrUpdateBankAccountBalance( + bankId: BankId, + accountId: AccountId, + balanceId: Option[BalanceId], + balanceType: String, + balanceAmount: BigDecimal, + callContext: Option[CallContext] + ): OBPReturnType[Box[BankAccountBalanceTrait]] = Future{(Failure(setUnimplementedError(nameOf(createOrUpdateBankAccountBalance _))), callContext)} + + def deleteBankAccountBalance( + balanceId: BalanceId, + callContext: Option[CallContext] + ): OBPReturnType[Box[Boolean]] = Future{(Failure(setUnimplementedError(nameOf(deleteBankAccountBalance _))), callContext)} } diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 0ee18569f..c10405e2a 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -32,6 +32,7 @@ import code.customeraddress.CustomerAddressX import code.customerattribute.CustomerAttributeX import code.directdebit.DirectDebits import code.endpointTag.EndpointTag +import code.bankaccountbalance.BankAccountBalanceX import com.openbankproject.commons.model.EndpointTagT import code.fx.{MappedFXRate, fx} import code.kycchecks.KycChecks @@ -5362,6 +5363,52 @@ object LocalMappedConnector extends Connector with MdcLoggable { MappedRegulatedEntityProvider.getRegulatedEntityByEntityId(regulatedEntityId) } map { (_, callContext) - } + } + + override def getBankAccountBalancesByAccountId( + accountId: AccountId, + callContext: Option[CallContext] + ): OBPReturnType[Box[List[BankAccountBalanceTrait]]] = { + BankAccountBalanceX.bankAccountBalanceProvider.vend.getBankAccountBalances(accountId).map { + (_, callContext) + } + } + + override def getBankAccountBalanceById( + balanceId: BalanceId, + callContext: Option[CallContext] + ): OBPReturnType[Box[BankAccountBalanceTrait]] = { + BankAccountBalanceX.bankAccountBalanceProvider.vend.getBankAccountBalanceById(balanceId).map { + (_, callContext) + } + } + + override def createOrUpdateBankAccountBalance( + bankId: BankId, + accountId: AccountId, + balanceId: Option[BalanceId], + balanceType: String, + balanceAmount: BigDecimal, + callContext: Option[CallContext] + ): OBPReturnType[Box[BankAccountBalanceTrait]] = { + BankAccountBalanceX.bankAccountBalanceProvider.vend.createOrUpdateBankAccountBalance( + bankId, + accountId, + balanceId, + balanceType, + balanceAmount + ).map { + (_, callContext) + } + } + + override def deleteBankAccountBalance( + balanceId: BalanceId, + callContext: Option[CallContext] + ): OBPReturnType[Box[Boolean]] = { + BankAccountBalanceX.bankAccountBalanceProvider.vend.deleteBankAccountBalance(balanceId).map { + (_, callContext) + } + } } diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index a4cbc372e..674c4aed1 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -158,6 +158,15 @@ class WebUI extends MdcLoggable{ def aboutText: CssSel = { "#main-about-text *" #> scala.xml.Unparsed(getWebUiPropsValue("webui_index_page_about_section_text", "")) } + + def aLiveHtml: CssSel = { + "#get-disabled-versions *" #> scala.xml.Unparsed(APIUtil.getDisabledVersions.toString())& + "#get-enabled-versions *" #> scala.xml.Unparsed(APIUtil.getEnabledVersions.toString())& + "#get-disabled-endpoint-operation-ids *" #> scala.xml.Unparsed(APIUtil.getDisabledEndpointOperationIds.toString())& + "#get-enabled-endpoint-operation-ids *" #> scala.xml.Unparsed(APIUtil.getEnabledEndpointOperationIds.toString())& + "#alive-disabled-api-mode *" #> scala.xml.Unparsed(getWebUiPropsValue("server_mode", "apis,portal")) + } + def topText: CssSel = { "#top-text *" #> scala.xml.Unparsed(getWebUiPropsValue("webui_top_text", "")) diff --git a/obp-api/src/main/webapp/alive.html b/obp-api/src/main/webapp/alive.html new file mode 100644 index 000000000..d6dbbd683 --- /dev/null +++ b/obp-api/src/main/webapp/alive.html @@ -0,0 +1,14 @@ +
+
+

Disabled Versions:

+
+

Enabled Versions:

+
+

Disabled Endpoint Operation Ids:

+
+

Enabled Endpoint Operation Ids:

+
+

API Mode:

+
+
+
\ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v5_1_0/BankAccountBalanceTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/BankAccountBalanceTest.scala new file mode 100644 index 000000000..0d8b260c4 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v5_1_0/BankAccountBalanceTest.scala @@ -0,0 +1,162 @@ +package code.api.v5_1_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages +import code.api.v5_1_0.APIMethods510.Implementations5_1_0 +import code.entitlement.Entitlement +import code.setup.DefaultUsers +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + +class BankAccountBalanceTest extends V510ServerSetup with DefaultUsers { + + object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString) + object Create extends Tag(nameOf(Implementations5_1_0.createBankAccountBalance)) + object Update extends Tag(nameOf(Implementations5_1_0.updateBankAccountBalance)) + object Delete extends Tag(nameOf(Implementations5_1_0.deleteBankAccountBalance)) + object GetAll extends Tag(nameOf(Implementations5_1_0.getAllBankAccountBalances)) + object GetOne extends Tag(nameOf(Implementations5_1_0.getBankAccountBalanceById)) + + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + lazy val balanceId = createMockBalance(bankId, accountId) + + def createMockBalance(bankId: String, accountId: String): String = { + val entitlement = Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanCreateBankAccountBalance.toString) + val json = bankAccountBalanceRequestJsonV510 + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances").POST <@ user1 + val response = makePostRequest(request, write(json)) + Entitlement.entitlement.vend.deleteEntitlement(entitlement) + (response.body.extract[BankAccountBalanceResponseJsonV510].balance_id) + } + + feature("Create Bank Account Balance") { + + scenario("401 Unauthorized", Create, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances").POST + val response = makePostRequest(request, write(bankAccountBalanceRequestJsonV510)) + response.code should equal(401) + response.body.extract[ErrorMessage].message should equal(ErrorMessages.UserNotLoggedIn) + } + + scenario("403 Forbidden (no role)", Create, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances").POST <@ user1 + val response = makePostRequest(request, write(bankAccountBalanceRequestJsonV510)) + response.code should equal(403) + response.body.extract[ErrorMessage].message should startWith(ErrorMessages.UserHasMissingRoles + CanCreateBankAccountBalance.toString) + } + + scenario("201 Success + Field Echo", Create, VersionOfApi) { + val entitlement = Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanCreateBankAccountBalance.toString) + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances").POST <@ user1 + val response = makePostRequest(request, write(bankAccountBalanceRequestJsonV510)) + response.code should equal(201) + val created = response.body.extract[BankAccountBalanceResponseJsonV510] + created.balance_type should equal(bankAccountBalanceRequestJsonV510.balance_type) + created.balance_amount should equal(bankAccountBalanceRequestJsonV510.balance_amount) + created.account_id should equal(accountId) + Entitlement.entitlement.vend.deleteEntitlement(entitlement) + } + } + + feature("Update Bank Account Balance") { + + scenario("401 Unauthorized", Update, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).PUT + val response = makePutRequest(request, write(bankAccountBalanceRequestJsonV510)) + response.code should equal(401) + } + + scenario("403 Forbidden", Update, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).PUT <@ user1 + val response = makePutRequest(request, write(bankAccountBalanceRequestJsonV510)) + response.code should equal(403) + } + + scenario("200 Success", Update, VersionOfApi) { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + lazy val balanceId = createMockBalance(bankId, accountId) + + val entitlement = Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanUpdateBankAccountBalance.toString) + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).PUT <@ user1 + val response = makePutRequest(request, write(bankAccountBalanceRequestJsonV510)) + response.code should equal(200) + Entitlement.entitlement.vend.deleteEntitlement(entitlement) + } + } + + feature("Delete Bank Account Balance") { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + lazy val balanceId = createMockBalance(bankId, accountId) + + scenario("401 Unauthorized", Delete, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).DELETE + val response = makeDeleteRequest(request) + response.code should equal(401) + } + + scenario("403 Forbidden", Delete, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).DELETE <@ user1 + val response = makeDeleteRequest(request) + response.code should equal(403) + } + + scenario("204 Success", Delete, VersionOfApi) { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + lazy val balanceId = createMockBalance(bankId, accountId) + val entitlement = Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanDeleteBankAccountBalance.toString) + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).DELETE <@ user1 + val response = makeDeleteRequest(request) + response.code should equal(204) + Entitlement.entitlement.vend.deleteEntitlement(entitlement) + } + } + + feature("Get All Bank Account Balances") { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + lazy val balanceId = createMockBalance(bankId, accountId) + + scenario("401 Unauthorized", GetAll, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances").GET + val response = makeGetRequest(request) + response.code should equal(401) + } + + scenario("200 Success", GetAll, VersionOfApi) { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances").GET <@ user1 + val response = makeGetRequest(request) + response.code should equal(200) + } + } + + feature("Get Bank Account Balance by ID") { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + + scenario("401 Unauthorized", GetOne, VersionOfApi) { + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).GET + val response = makeGetRequest(request) + response.code should equal(401) + } + + scenario("200 Success", GetOne, VersionOfApi) { + lazy val bankId = testBankId1.value + lazy val accountId = testAccountId1.value + lazy val balanceId = createMockBalance(bankId, accountId) + val request = (v5_1_0_Request / "banks" / bankId / "accounts" / accountId / "balances" / balanceId).GET <@ user1 + val response = makeGetRequest(request) + response.code should equal(200) + } + } +} \ No newline at end of file diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala index 06a1f0a0e..caf5613a1 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/BankingModel.scala @@ -136,6 +136,14 @@ object RegulatedEntityId { def unapply(id : String) = Some(RegulatedEntityId(id)) } +case class BalanceId(val value : String) { + override def toString = value +} + +object BalanceId { + def unapply(id : String) = Some(BalanceId(id)) +} + case class AccountId(val value : String) { override def toString = value } @@ -228,6 +236,14 @@ trait BankAccount{ def attributes : Option[List[Attribute]] = None } +trait BankAccountBalanceTrait { + def bankId: BankId + def accountId: AccountId + def balanceId: BalanceId + def balanceType: String + def balanceAmount: BigDecimal +} + //This class is used for propagate the BankAccount as the parameters over different methods. case class BankAccountInMemory( //BankAccount Trait diff --git a/release_notes.md b/release_notes.md index d5dbc5dff..cbd940748 100644 --- a/release_notes.md +++ b/release_notes.md @@ -189,7 +189,7 @@ Date Commit Action 02/05/2017 3084827 added 1 new caching props to sample.props.template api.cache.ttl.seconds.APIMethods121.getTransactions. If it's omitted default value is 0 i.e. no caching. This cacahe is from API level. 10/05/2017 7f95a5c added allow_public_views=false, we will not create the public views and will not access them (if public views are exsiting)when it is false. 17/07/2017 1530231 added account_id.length=64, this will set all relevant accountid length to 64, when create new sandbox. -17/02/2016 e3bead1 Added Props defaultBank.bank_id. Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID +17/02/2016 e3bead1 Added Props defaultBank.bank_id. Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID. The default value is OBP. ```