diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 9847912b0..8bbaabc03 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1118,6 +1118,11 @@ default_auth_context_update_request_key=CUSTOMER_NUMBER # the alias prefix path for BerlinGroupV1.3 (OBP built-in is berlin-group/v1.3), the format must be xxx/yyy, eg: 0.6/v1 #berlin_group_v1.3_alias.path= +# Show the path inside of Berlin Group error message +#berlin_group_error_message_show_path = true + +## Berlin Group Create Consent Frequency per Day Upper Limit +#berlin_group_frequency_per_day_upper_limit = 4 # Support multiple brands on one instance. Note this needs checking on a clustered environment #brands_enabled=false 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 b1a267792..74b39d19b 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 @@ -4,7 +4,6 @@ import code.api.Constant import code.api.Constant._ import code.api.UKOpenBanking.v2_0_0.JSONFactory_UKOpenBanking_200 import code.api.UKOpenBanking.v2_0_0.JSONFactory_UKOpenBanking_200.{Account, AccountBalancesUKV200, AccountInner, AccountList, Accounts, BalanceJsonUKV200, BalanceUKOpenBankingJson, BankTransactionCodeJson, CreditLineJson, DataJsonUKV200, Links, MetaBisJson, MetaInnerJson, TransactionCodeJson, TransactionInnerJson, TransactionsInnerJson, TransactionsJsonUKV200} -import code.api.berlin.group.v1.JSONFactory_BERLIN_GROUP_1.{AccountBalanceV1, AccountBalances, AmountOfMoneyV1, ClosingBookedBody, ExpectedBody, TransactionJsonV1, TransactionsJsonV1, ViewAccount} import code.api.dynamic.endpoint.helper.practise.PractiseEndpoint import code.api.util.APIUtil.{defaultJValue, _} import code.api.util.ApiRole._ @@ -12,8 +11,8 @@ import code.api.util.ExampleValue._ import code.api.util.{ApiRole, ApiTrigger, ExampleValue} import code.api.v2_2_0.JSONFactory220.{AdapterImplementationJson, MessageDocJson, MessageDocsJson} import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 -import code.api.v3_0_0.custom.JSONFactoryCustom300 import code.api.v3_0_0._ +import code.api.v3_0_0.custom.JSONFactoryCustom300 import code.api.v3_1_0._ import code.api.v4_0_0._ import code.api.v5_0_0._ @@ -27,9 +26,9 @@ import code.sandbox.SandboxData import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model import com.openbankproject.commons.model.PinResetReason.{FORGOT, GOOD_SECURITY_PRACTICE} +import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.TransactionRequestTypes._ import com.openbankproject.commons.model.enums.{AttributeCategory, CardAttributeType, ChallengeType} -import com.openbankproject.commons.model._ import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils} import net.liftweb.json @@ -3477,10 +3476,6 @@ object SwaggerDefinitionsJSON { val coreAccountsJsonV300 = CoreAccountsJsonV300(accounts = List(coreAccountJson)) - val amountOfMoneyV1 = AmountOfMoneyV1( - currency = "String", - content = "String" - ) val accountInnerJsonUKOpenBanking_v200 = AccountInner( SchemeName = "SortCodeAccountNumber", @@ -3510,43 +3505,6 @@ object SwaggerDefinitionsJSON { Meta = metaUK ) - val closingBookedBody = ClosingBookedBody( - amount = amountOfMoneyV1, - date = "2017-10-25" - ) - - val expectedBody = ExpectedBody( - amount = amountOfMoneyV1, - lastActionDateTime = DateWithDayExampleObject - ) - - val accountBalanceV1 = AccountBalanceV1( - closingBooked = closingBookedBody, - expected = expectedBody - ) - - val accountBalances = AccountBalances( - `balances` = List(accountBalanceV1) - ) - - val transactionJsonV1 = TransactionJsonV1( - transactionId = "String", - creditorName = "String", - creditorAccount = ibanJson, - amount = amountOfMoneyV1, - bookingDate = DateWithDayExampleObject, - valueDate = DateWithDayExampleObject, - remittanceInformationUnstructured = "String" - ) - - val viewAccount = ViewAccount(viewAccount = "/v1/accounts/3dc3d5b3-7023-4848-9853- f5400a64e80f") - - val transactionsJsonV1 = TransactionsJsonV1( - transactions_booked = List(transactionJsonV1), - transactions_pending = List(transactionJsonV1), - _links = List(viewAccount) - ) - val accountIdJson = AccountIdJson( id = "5995d6a2-01b3-423c-a173-5481df49bdaf" ) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index a1b3da664..8e69aab36 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -22,7 +22,6 @@ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{NotSupportedYet, notSu import code.api.STET.v1_4.OBP_STET_1_4 import code.api.UKOpenBanking.v2_0_0.OBP_UKOpenBanking_200 import code.api.UKOpenBanking.v3_1_0.OBP_UKOpenBanking_310 -import code.api.berlin.group.v1.OBP_BERLIN_GROUP_1 import code.api.berlin.group.v1_3.{OBP_BERLIN_GROUP_1_3, OBP_BERLIN_GROUP_1_3_Alias} import code.api.v1_4_0.JSONFactory1_4_0 import com.openbankproject.commons.model.JsonFieldReName @@ -294,7 +293,6 @@ object SwaggerJSONFactory extends MdcLoggable { "Creative Commons Attribution 3.0 Australia (CC BY 3.0 AU)" else if (apiVersion == OBP_BERLIN_GROUP_1_3.apiVersion || apiVersion == OBP_BERLIN_GROUP_1_3_Alias.apiVersion - || apiVersion == OBP_BERLIN_GROUP_1.apiVersion ) "Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND)" else s"License: Unknown" diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1/APIMethods_BERLIN_GROUP_1.scala b/obp-api/src/main/scala/code/api/berlin/group/v1/APIMethods_BERLIN_GROUP_1.scala deleted file mode 100644 index 3c7b92fd2..000000000 --- a/obp-api/src/main/scala/code/api/berlin/group/v1/APIMethods_BERLIN_GROUP_1.scala +++ /dev/null @@ -1,186 +0,0 @@ -package code.api.berlin.group.v1 - -import code.api.APIFailureNewStyle -import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON -import code.api.berlin.group.v1.JSONFactory_BERLIN_GROUP_1.{Balances, CoreAccountJsonV1, CoreAccountsJsonV1, Transactions} -import code.api.util.APIUtil.{defaultBankId, _} -import code.api.util.{NewStyle} -import code.api.util.ErrorMessages._ -import code.api.util.ApiTag._ -import code.api.util.NewStyle.HttpCode -import code.bankconnectors.Connector -import code.model._ -import code.util.Helper -import code.views.Views -import com.openbankproject.commons.model.{AccountId, BankId, BankIdAccountId, ViewId} -import net.liftweb.common.Full -import net.liftweb.http.rest.RestHelper - -import scala.collection.immutable.Nil -import scala.collection.mutable.ArrayBuffer -import com.openbankproject.commons.ExecutionContext.Implicits.global -import scala.concurrent.Future - -object APIMethods_BERLIN_GROUP_1 extends RestHelper{ - val implementedInApiVersion = OBP_BERLIN_GROUP_1.apiVersion - - val resourceDocs = ArrayBuffer[ResourceDoc]() - val apiRelations = ArrayBuffer[ApiRelation]() - val codeContext = CodeContext(resourceDocs, apiRelations) - - - resourceDocs += ResourceDoc( - getAccountList, - implementedInApiVersion, - "getAccountList", - "GET", - "/accounts", - "Berlin Group: Read Account List", - s""" - |Reads a list of bank accounts, with balances where required. - |It is assumed that a consent of the PSU to this access is already given and stored on the ASPSP system. - | - |${userAuthenticationMessage(true)} - | - |This endpoint is work in progress. Experimental! - |""", - EmptyBody, - CoreAccountsJsonV1(List(CoreAccountJsonV1( - id = "3dc3d5b3-7023-4848-9853-f5400a64e80f", - iban = "DE2310010010123456789", - currency = "EUR", - accountType = "Girokonto", - cashAccountType = "CurrentAccount", - _links = List( - Balances("/v1/accounts/3dc3d5b3-7023-4848-9853-f5400a64e80f/balances"), - Transactions("/v1/accounts/3dc3d5b3-7023-4848-9853-f5400a64e80f/transactions") - ), - name = "Main Account" - ))), - List(UserNotLoggedIn,UnknownError), - List(apiTagBerlinGroup, apiTagAccount, apiTagPrivateData, apiTagPsd2)) - - - apiRelations += ApiRelation(getAccountList, getAccountList, "self") - - - - lazy val getAccountList : OBPEndpoint = { - //get private accounts for one bank - case "accounts" :: Nil JsonGet _ => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - - _ <- Helper.booleanToFuture(failMsg= DefaultBankIdNotSet, cc=callContext) {defaultBankId != "DEFAULT_BANK_ID_NOT_SET"} - - bankId = BankId(defaultBankId) - - (_, callContext) <- NewStyle.function.getBank(bankId, callContext) - - availablePrivateAccounts <- Views.views.vend.getPrivateBankAccountsFuture(u, bankId) - - Full((coreAccounts,callContext1)) <- {Connector.connector.vend.getCoreBankAccounts(availablePrivateAccounts, callContext)} - - } yield { - (JSONFactory_BERLIN_GROUP_1.createTransactionListJSON(coreAccounts), callContext) - } - } - } - - resourceDocs += ResourceDoc( - getAccountBalances, - implementedInApiVersion, - "getAccountBalances", - "GET", - "/accounts/ACCOUNT_ID/balances", - "Berlin Group Read Balance", - s""" - |Reads account data from a given account addressed by “account-id”. - | - |${userAuthenticationMessage(true)} - | - |This endpoint is work in progress. Experimental! - |""", - EmptyBody, - SwaggerDefinitionsJSON.accountBalances, - List(UserNotLoggedIn, ViewNotFound, UserNoPermissionAccessView, UnknownError), - List(apiTagBerlinGroup, apiTagAccount, apiTagPrivateData, apiTagPsd2)) - - lazy val getAccountBalances : OBPEndpoint = { - //get private accounts for all banks - case "accounts" :: AccountId(accountId) :: "balances" :: Nil JsonGet _ => { - cc => - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- Helper.booleanToFuture(failMsg= DefaultBankIdNotSet, cc=callContext) { defaultBankId != "DEFAULT_BANK_ID_NOT_SET" } - (_, callContext) <- NewStyle.function.getBank(BankId(defaultBankId), callContext) - (bankAccount, callContext) <- NewStyle.function.checkBankAccountExists(BankId(defaultBankId), accountId, callContext) - view <- NewStyle.function.checkOwnerViewAccessAndReturnOwnerView(u, BankIdAccountId(bankAccount.bankId, bankAccount.accountId), callContext) - (transactionRequests, callContext) <- Future { Connector.connector.vend.getTransactionRequests210(u, bankAccount, callContext)} map { - x => fullBoxOrException(x ~> APIFailureNewStyle(InvalidConnectorResponseForGetTransactionRequests210, 400, callContext.map(_.toLight))) - } map { unboxFull(_) } - moderatedAccount <- Future {bankAccount.moderatedBankAccount(view, BankIdAccountId(bankAccount.bankId, accountId), Full(u), callContext)} map { - x => fullBoxOrException(x ~> APIFailureNewStyle(UnknownError, 400, callContext.map(_.toLight))) - } map { unboxFull(_) } - } yield { - (JSONFactory_BERLIN_GROUP_1.createAccountBalanceJSON(moderatedAccount, transactionRequests), HttpCode.`200`(callContext)) - } - } - } - - resourceDocs += ResourceDoc( - getTransactionList, - implementedInApiVersion, - "getTransactionList", - "GET", - "/accounts/ACCOUNT_ID/transactions", - "Berlin Group Read Account Transactions", - s""" - |Reads account data from a given account addressed by “account-id”. - |${userAuthenticationMessage(true)} - | - |This endpoint is work in progress. Experimental! - |""", - EmptyBody, - SwaggerDefinitionsJSON.transactionsJsonV1, - List(UserNotLoggedIn,UnknownError), - List(apiTagBerlinGroup, apiTagTransaction, apiTagPrivateData, apiTagPsd2)) - - lazy val getTransactionList : OBPEndpoint = { - //get private accounts for all banks - case "accounts" :: AccountId(accountId) :: "transactions" :: Nil JsonGet _ => { - cc => - for { - - (Full(u), callContext) <- authenticatedAccess(cc) - - _ <- Helper.booleanToFuture(failMsg= DefaultBankIdNotSet, cc=callContext) {defaultBankId != "DEFAULT_BANK_ID_NOT_SET"} - - bankId = BankId(defaultBankId) - - (bank, callContext) <- NewStyle.function.getBank(bankId, callContext) - - (bankAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, accountId, callContext) - - view <- NewStyle.function.checkOwnerViewAccessAndReturnOwnerView(u, BankIdAccountId(bankAccount.bankId, bankAccount.accountId), callContext) - - params <- Future { createQueriesByHttpParams(callContext.get.requestHeaders)} map { - x => fullBoxOrException(x ~> APIFailureNewStyle(UnknownError, 400, callContext.map(_.toLight))) - } map { unboxFull(_) } - - (transactionRequests, callContext) <- Future { Connector.connector.vend.getTransactionRequests210(u, bankAccount, callContext)} map { - x => fullBoxOrException(x ~> APIFailureNewStyle(InvalidConnectorResponseForGetTransactionRequests210, 400, callContext.map(_.toLight))) - } map { unboxFull(_) } - - (transactions, callContext) <- Future { bankAccount.getModeratedTransactions(bank, Full(u), view, BankIdAccountId(bankId, accountId), callContext, params)} map { - x => fullBoxOrException(x ~> APIFailureNewStyle(UnknownError, 400, callContext.map(_.toLight))) - } map { unboxFull(_) } - - } yield { - (JSONFactory_BERLIN_GROUP_1.createTransactionsJson(transactions, transactionRequests), callContext) - } - } - } - -} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1/JSONFactory_BERLIN_GROUP_1.scala b/obp-api/src/main/scala/code/api/berlin/group/v1/JSONFactory_BERLIN_GROUP_1.scala deleted file mode 100644 index bf66cecd2..000000000 --- a/obp-api/src/main/scala/code/api/berlin/group/v1/JSONFactory_BERLIN_GROUP_1.scala +++ /dev/null @@ -1,141 +0,0 @@ -package code.api.berlin.group.v1 - -import java.util.Date - -import code.api.util.{APIUtil, CustomJsonFormats} -import code.api.v2_1_0.IbanJson -import code.model.{ModeratedBankAccount, ModeratedTransaction} -import com.openbankproject.commons.model.{CoreAccount, TransactionRequest} - -import scala.collection.immutable.List - -object JSONFactory_BERLIN_GROUP_1 extends CustomJsonFormats { - - /** - * why links is class instead of trait? - * because when do swagger file generation, there is no way to define a type of links, because all the implementations - * have different structure. if it is abstract we have no way to define an doc example - */ - trait links - case class Balances(balances: String) extends links - case class Transactions(transactions: String) extends links - case class ViewAccount(viewAccount: String) extends links - case class CoreAccountJsonV1( - id: String, - iban: String, - currency: String, - accountType: String, - cashAccountType: String, - _links: List[links], - name: String - ) - - case class CoreAccountsJsonV1(`account-list`: List[CoreAccountJsonV1]) - - case class AmountOfMoneyV1( - currency : String, - content : String - ) - case class ClosingBookedBody( - amount : AmountOfMoneyV1, - date: String //eg: “2017-10-25”, this is not a valid datetime (not java.util.Date) - ) - case class ExpectedBody( - amount : AmountOfMoneyV1, - lastActionDateTime: Date - ) - case class AccountBalanceV1( - closingBooked: ClosingBookedBody, - expected: ExpectedBody - ) - case class AccountBalances(`balances`: List[AccountBalanceV1]) - - case class TransactionsJsonV1( - transactions_booked: List[TransactionJsonV1], - transactions_pending: List[TransactionJsonV1], - _links: List[ViewAccount] - ) - - case class TransactionJsonV1( - transactionId: String, - creditorName: String, - creditorAccount: IbanJson, - amount: AmountOfMoneyV1, - bookingDate: Date, - valueDate: Date, - remittanceInformationUnstructured: String - ) - - def createTransactionListJSON(coreAccounts: List[CoreAccount]): CoreAccountsJsonV1 = { - CoreAccountsJsonV1(coreAccounts.map( - x => CoreAccountJsonV1( - id = x.id, - iban = if (x.accountRoutings.headOption.isDefined && x.accountRoutings.head.scheme == "IBAN") x.accountRoutings.head.address else "", - currency = "", - accountType = "", - cashAccountType = "", - _links = Balances(s"/${OBP_BERLIN_GROUP_1.version}/accounts/${x.id}/balances") :: Transactions(s"/${OBP_BERLIN_GROUP_1.version}/accounts/${x.id}/transactions") :: Nil, - name = x.label) - ) - ) - } - - def createAccountBalanceJSON(moderatedAccount: ModeratedBankAccount, transactionRequests: List[TransactionRequest]) = { - // get the latest end_date of `COMPLETED` transactionRequests - val latestCompletedEndDate = transactionRequests.sortBy(_.end_date).reverse.filter(_.status == "COMPLETED").map(_.end_date).headOption.getOrElse(null) - - //get the latest end_date of !`COMPLETED` transactionRequests - val latestUncompletedEndDate = transactionRequests.sortBy(_.end_date).reverse.filter(_.status != "COMPLETED").map(_.end_date).headOption.getOrElse(null) - - // get the SUM of the amount of all !`COMPLETED` transactionRequests - val sumOfAllUncompletedTransactionrequests = transactionRequests.filter(_.status != "COMPLETED").map(_.body.value.amount).map(BigDecimal(_)).sum - // sum of the unCompletedTransactions and the account.balance is the current expectd amount: - val sumOfAll = (BigDecimal(moderatedAccount.balance) + sumOfAllUncompletedTransactionrequests).toString() - - AccountBalances( - AccountBalanceV1( - closingBooked = ClosingBookedBody( - amount = AmountOfMoneyV1(currency = moderatedAccount.currency.getOrElse(""), content = moderatedAccount.balance), - date = APIUtil.DateWithDayFormat.format(latestCompletedEndDate) - ), - expected = ExpectedBody( - amount = AmountOfMoneyV1(currency = moderatedAccount.currency.getOrElse(""), - content = sumOfAll), - lastActionDateTime = latestUncompletedEndDate) - ) :: Nil - ) - } - - def createTransactionJSON(transaction : ModeratedTransaction) : TransactionJsonV1 = { - TransactionJsonV1( - transactionId = transaction.id.value, - creditorName = "", - creditorAccount = IbanJson(APIUtil.stringOptionOrNull(transaction.bankAccount.get.iban)), - amount = AmountOfMoneyV1(APIUtil.stringOptionOrNull(transaction.currency), transaction.amount.get.toString()), - bookingDate = transaction.startDate.get, - valueDate = transaction.finishDate.get, - remittanceInformationUnstructured = APIUtil.stringOptionOrNull(transaction.description) - ) - } - - def createTransactionFromRequestJSON(transactionRequest : TransactionRequest) : TransactionJsonV1 = { - TransactionJsonV1( - transactionId = transactionRequest.id.value, - creditorName = transactionRequest.name, - creditorAccount = IbanJson(transactionRequest.from.account_id), - amount = AmountOfMoneyV1(transactionRequest.charge.value.currency, transactionRequest.charge.value.amount), - bookingDate = transactionRequest.start_date, - valueDate = transactionRequest.end_date, - remittanceInformationUnstructured = transactionRequest.body.description - ) - } - - def createTransactionsJson(transactions: List[ModeratedTransaction], transactionRequests: List[TransactionRequest]) : TransactionsJsonV1 = { - TransactionsJsonV1( - transactions_booked =transactions.map(createTransactionJSON), - transactions_pending =transactionRequests.filter(_.status!="COMPLETED").map(createTransactionFromRequestJSON), - _links = ViewAccount(s"/${OBP_BERLIN_GROUP_1.version}/accounts/${transactionRequests.head.from.account_id}/balances")::Nil - ) - } - -} diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1/OBP_BERLIN_GROUP_1.scala b/obp-api/src/main/scala/code/api/berlin/group/v1/OBP_BERLIN_GROUP_1.scala deleted file mode 100644 index 34936c61f..000000000 --- a/obp-api/src/main/scala/code/api/berlin/group/v1/OBP_BERLIN_GROUP_1.scala +++ /dev/null @@ -1,68 +0,0 @@ -/** -Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ -package code.api.berlin.group.v1 - -import code.api.OBPRestHelper -import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints} -import com.openbankproject.commons.util.{ApiVersionStatus, ScannedApiVersion} -import code.api.util.ScannedApis -import code.util.Helper.MdcLoggable -import code.api.berlin.group.v1.APIMethods_BERLIN_GROUP_1._ - -import scala.collection.immutable.Nil - - - -/* -This file defines which endpoints from all the versions are available in v1 - */ - - -object OBP_BERLIN_GROUP_1 extends OBPRestHelper with MdcLoggable with ScannedApis{ - - override val apiVersion = ScannedApiVersion("berlin-group", "BG", "v1") - val versionStatus = ApiVersionStatus.DRAFT.toString - - val allEndpoints = - getAccountList :: - getAccountBalances :: - getAccountBalances :: - getTransactionList :: - Nil - - override val allResourceDocs = resourceDocs - - // Filter the possible endpoints by the disabled / enabled Props settings and add them together - override val routes : List[OBPEndpoint] = getAllowedEndpoints(allEndpoints,resourceDocs) - - - // Make them available for use! - registerRoutes(routes, allResourceDocs, apiPrefix) - - logger.info(s"version $apiVersion has been run! There are ${routes.length} routes.") - -} diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index bb37fd533..4ab4828c2 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -5,7 +5,7 @@ import code.api.APIFailureNewStyle import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID} import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{PostConsentResponseJson, _} import code.api.berlin.group.v1_3.model.{HrefType, LinksAll, ScaStatusResponse} -import code.api.berlin.group.v1_3.{JSONFactory_BERLIN_GROUP_1_3, JvalueCaseClass, OBP_BERLIN_GROUP_1_3} +import code.api.berlin.group.v1_3.{BgSpecValidation, JSONFactory_BERLIN_GROUP_1_3, JvalueCaseClass, OBP_BERLIN_GROUP_1_3} import code.api.berlin.group.v1_3.model._ import code.api.util.APIUtil.{passesPsd2Aisp, _} import code.api.util.ApiTag._ @@ -106,6 +106,15 @@ This option is not supported for the Embedded SCA Approach. As a last option, an ASPSP might in addition accept a command with access rights * to see the list of available payment accounts or * to see the list of available payment accounts with balances. + +frequencyPerDay: + This field indicates the requested maximum frequency for an access without PSU involvement per day. + For a one-off access, this attribute is set to "1". + The frequency needs to be greater equal to one. + If not otherwise agreed bilaterally between TPP and ASPSP, the frequency is less equal to 4. +recurringIndicator: + "true", if the consent is for recurring access to the account data. + "false", if the consent is for one access to the account data. """, PostConsentJson( access = ConsentAccessJson( @@ -125,7 +134,7 @@ As a last option, an ASPSP might in addition accept a command with access rights recurringIndicator = true, validUntil = "2020-12-31", frequencyPerDay = 4, - combinedServiceIndicator = false + combinedServiceIndicator = Some(false) ), PostConsentResponseJson( consentId = "1234-wertiq-983", @@ -150,9 +159,9 @@ As a last option, an ASPSP might in addition accept a command with access rights consentJson <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostConsentJson] } - + upperLimit = APIUtil.getPropsAsIntValue("berlin_group_frequency_per_day_upper_limit", 4) _ <- Helper.booleanToFuture(failMsg = FrequencyPerDayError, cc=callContext) { - consentJson.frequencyPerDay > 0 + consentJson.frequencyPerDay > 0 && consentJson.frequencyPerDay <= upperLimit } _ <- Helper.booleanToFuture(failMsg = FrequencyPerDayMustBeOneError, cc=callContext) { @@ -160,9 +169,10 @@ As a last option, an ASPSP might in addition accept a command with access rights (consentJson.recurringIndicator == false && consentJson.frequencyPerDay == 1) } - failMsg = s"$InvalidDateFormat Current `validUntil` field is ${consentJson.validUntil}. Please use this format ${DateWithDayFormat.toPattern}!" - validUntil <- NewStyle.function.tryons(failMsg, 400, callContext) { - new SimpleDateFormat(DateWithDay).parse(consentJson.validUntil) + failMsg = BgSpecValidation.getErrorMessage(consentJson.validUntil) + validUntil = BgSpecValidation.getDate(consentJson.validUntil) + _ <- Helper.booleanToFuture(failMsg, 400, callContext) { + failMsg.isEmpty } _ <- NewStyle.function.getBankAccountsByIban(consentJson.access.accounts.getOrElse(Nil).map(_.iban.getOrElse("")), callContext) @@ -173,7 +183,7 @@ As a last option, an ASPSP might in addition accept a command with access rights recurringIndicator = consentJson.recurringIndicator, validUntil = validUntil, frequencyPerDay = consentJson.frequencyPerDay, - combinedServiceIndicator = consentJson.combinedServiceIndicator, + combinedServiceIndicator = consentJson.combinedServiceIndicator.getOrElse(false), apiStandard = Some(apiVersion.apiStandard), apiVersion = Some(apiVersion.apiShortVersion) )) map { diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala new file mode 100644 index 000000000..821327b35 --- /dev/null +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala @@ -0,0 +1,59 @@ +package code.api.berlin.group.v1_3 + +import code.api.util.APIUtil.DateWithDayFormat +import code.api.util.ErrorMessages.InvalidDateFormat + +import java.time.format.{DateTimeFormatter, DateTimeParseException} +import java.time.{LocalDate, ZoneId} +import java.util.Date + +object BgSpecValidation { + + val MaxValidDate: LocalDate = LocalDate.parse("9999-12-31") + val DateFormat: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE + + def getErrorMessage(dateStr: String): String = { + validateValidUntil(dateStr) match { + case Right(validDate) => "" + case Left(error) => error + } + } + def getDate(dateStr: String): Date = { + validateValidUntil(dateStr) match { + case Right(validDate) => + Date.from(validDate.atStartOfDay(ZoneId.systemDefault).toInstant) + case Left(error) => + null + } + } + private def validateValidUntil(dateStr: String): Either[String, LocalDate] = { + try { + val date = LocalDate.parse(dateStr, DateFormat) + val today = LocalDate.now() + + if (date.isBefore(today)) { + Left(s"$InvalidDateFormat Current `validUntil` field is ${dateStr}. The date must not be in the past!") + } else if (date.isAfter(MaxValidDate)) { + Left(s"$InvalidDateFormat Current `validUntil` field is ${dateStr}. The maximum allowed date is $MaxValidDate.!") + } else { + Right(date) // Valid date + } + } catch { + case e: DateTimeParseException => + Left(s"$InvalidDateFormat Current `validUntil` field is ${dateStr}. Please use this format ${DateWithDayFormat.toPattern}!") + } + } + + // Example usage + def main(args: Array[String]): Unit = { + val testDates = Seq("2025-05-10", "9999-12-31", "2015-01-01", "invalid-date", "2025-01-20T11:04:20Z") + + testDates.foreach { date => + validateValidUntil(date) match { + case Right(validDate) => println(s"Valid date: $validDate") + case Left(error) => println(s"Error: $error") + } + } + } +} + diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 42705fdb4..846fc1d77 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -22,7 +22,7 @@ case class JvalueCaseClass(jvalueToCaseclass: JValue) object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { - case class ErrorMessageBG(category: String, code: Int, path: String, text: String) + case class ErrorMessageBG(category: String, code: String, path: Option[String], text: String) case class ErrorMessagesBG(tppMessages: List[ErrorMessageBG]) case class PostSigningBasketJsonV13( @@ -228,7 +228,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { recurringIndicator: Boolean, validUntil: String, frequencyPerDay: Int, - combinedServiceIndicator: Boolean + combinedServiceIndicator: Option[Boolean] ) case class ConsentLinksV13( startAuthorisation: Option[Href] = None, diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index af0006dac..6abdfd059 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -34,7 +34,6 @@ import code.api.OAuthHandshake._ import code.api.UKOpenBanking.v2_0_0.OBP_UKOpenBanking_200 import code.api.UKOpenBanking.v3_1_0.OBP_UKOpenBanking_310 import code.api._ -import code.api.berlin.group.v1.OBP_BERLIN_GROUP_1 import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ErrorMessageBG, ErrorMessagesBG} import code.api.cache.Caching import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint @@ -728,7 +727,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def composeErrorMessage() = { val path = callContextLight.map(_.url).getOrElse("") if (path.contains("berlin-group")) { - val errorMessagesBG = ErrorMessagesBG(tppMessages = List(ErrorMessageBG(category = "ERROR", code = code, path = callContextLight.map(_.url).getOrElse(""), text = message))) + val path = + if(APIUtil.getPropsAsBoolValue("berlin_group_error_message_show_path", defaultValue = true)) + callContextLight.map(_.url) + else + None + val errorMessagesBG = ErrorMessagesBG(tppMessages = List(ErrorMessageBG(category = "ERROR", code = code.toString, path = path, text = message))) Extraction.decompose(errorMessagesBG) } else { Extraction.decompose(ErrorMessage(message = message, code = code)) @@ -4843,7 +4847,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ ++ OBP_UKOpenBanking_310.allResourceDocs ++ code.api.Polish.v2_1_1_1.OBP_PAPI_2_1_1_1.allResourceDocs ++ code.api.STET.v1_4.OBP_STET_1_4.allResourceDocs - ++ OBP_BERLIN_GROUP_1.allResourceDocs ++ code.api.AUOpenBanking.v1_0_0.ApiCollector.allResourceDocs ++ code.api.MxOF.CNBV9_1_0_0.allResourceDocs ++ code.api.berlin.group.v1_3.OBP_BERLIN_GROUP_1_3.allResourceDocs diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 82d44e42e..e94ba0c73 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -809,7 +809,10 @@ object Consent extends MdcLoggable { if(views.isEmpty) { Empty } else { - val updatedPayload = payloadToUpdate.map(i => i.copy(views = views)) // Update only the field "views" + val updatedPayload = payloadToUpdate.map(i => + i.copy(views = views) // Update the field "views" + .copy(access = Some(access)) // Update the field "access" + ) val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload)) val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret)) 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 8e28a23ab..e26922d34 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -208,7 +208,7 @@ object ErrorMessages { val ConsumerIsDisabled = "OBP-20058: Consumer is disabled." val CouldNotAssignAccountAccess = "OBP-20059: Could not assign account access. " val NoViewReadAccountsBerlinGroup = s"OBP-20060: User does not have access to the view:" - val FrequencyPerDayError = "OBP-20062: Frequency per day must be greater than 0." + val FrequencyPerDayError = s"OBP-20062: Frequency per day must be greater than 0 and less or equal to ${APIUtil.getPropsAsIntValue("berlin_group_frequency_per_day_upper_limit", 4)}" val FrequencyPerDayMustBeOneError = "OBP-20063: Frequency per day must be equal to 1 in case of one-off access." val UserIsDeleted = "OBP-20064: The user is deleted!" diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1/BerlinGroupV1ServerSetup.scala b/obp-api/src/test/scala/code/api/berlin/group/v1/BerlinGroupV1ServerSetup.scala deleted file mode 100644 index 1174d7cec..000000000 --- a/obp-api/src/test/scala/code/api/berlin/group/v1/BerlinGroupV1ServerSetup.scala +++ /dev/null @@ -1,9 +0,0 @@ -package code.api.berlin.group.v1 - -import code.setup.ServerSetupWithTestData - -trait BerlinGroupV1ServerSetup extends ServerSetupWithTestData { - - def BerlinGroup_V1Request = baseRequest / "berlin-group" / "v1" - -} diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1/BerlinGroupV1Tests.scala b/obp-api/src/test/scala/code/api/berlin/group/v1/BerlinGroupV1Tests.scala deleted file mode 100644 index 879af6ecc..000000000 --- a/obp-api/src/test/scala/code/api/berlin/group/v1/BerlinGroupV1Tests.scala +++ /dev/null @@ -1,53 +0,0 @@ -package code.api.berlin.group.v1 - -import code.api.berlin.group.v1.JSONFactory_BERLIN_GROUP_1.{AccountBalances, CoreAccountsJsonV1, TransactionsJsonV1} -import code.api.util.APIUtil.OAuth._ -import code.setup.{APIResponse, DefaultUsers} -import org.scalatest.Tag - -class BerlinGroupV1Tests extends BerlinGroupV1ServerSetup with DefaultUsers { - - object BerlinGroup extends Tag("berlinGroup") - - feature("test the BG Read Account List") - { - scenario("Successful Case", BerlinGroup) - { - val requestGetAll = (BerlinGroup_V1Request / "accounts" ).GET <@(user1) - val response: APIResponse = makeGetRequest(requestGetAll) - - Then("We should get a 200 ") - response.code should equal(200) -// TODO because of the links is a trait, we can not extract automatically here. -// logger.info(response.body) -// val coreAccountsJsonV1 = response.body.extract[CoreAccountsJsonV1] - } - } - - feature("test the BG Read Balance") - { - scenario("Successful Case", BerlinGroup) - { - val requestGetAll = (BerlinGroup_V1Request / "accounts"/ testAccountId1.value /"balances" ).GET <@(user1) - val response = makeGetRequest(requestGetAll) - - Then("We should get a 200 ") - response.code should equal(200) - val accountBalances = response.body.extract[AccountBalances] - } - } - - feature("test the BG Read Account Transactions") - { - scenario("Successful Case", BerlinGroup) - { - val requestGetAll = (BerlinGroup_V1Request / "accounts"/ testAccountId1.value /"transactions" ).GET <@(user1) - val response = makeGetRequest(requestGetAll) - - Then("We should get a 200 ") - response.code should equal(200) - val transactionsJsonV1 = response.body.extract[TransactionsJsonV1] - } - } - -} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala index 5036ca8fb..d25e4da8b 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala @@ -17,6 +17,9 @@ import net.liftweb.json.Serialization.write import net.liftweb.mapper.By import org.scalatest.Tag +import java.time.LocalDate +import java.time.format.DateTimeFormatter + class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 with DefaultUsers { object getAccountList extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.getAccountList)) @@ -52,6 +55,10 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit object updateConsentsPsuDataUpdateSelectPsuAuthenticationMethod extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.updateConsentsPsuDataUpdateSelectPsuAuthenticationMethod)) object updateConsentsPsuDataUpdateAuthorisationConfirmation extends Tag(nameOf(APIMethods_AccountInformationServiceAISApi.updateConsentsPsuDataUpdateAuthorisationConfirmation)) + def getNextMonthDate(): String = { + val nextMonthDate = LocalDate.now().plusMonths(1) + nextMonthDate.format(DateTimeFormatter.ISO_LOCAL_DATE) + } feature(s"BG v1.3 - $getAccountList") { scenario("Not Authentication User, test failed ", BerlinGroupV1_3, getAccountList) { @@ -245,9 +252,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit allPsd2 = None ), recurringIndicator = true, - validUntil = "2020-12-31", + validUntil = getNextMonthDate(), frequencyPerDay = 4, - combinedServiceIndicator = false + combinedServiceIndicator = Some(false) ) val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) val response: APIResponse = makePostRequest(requestPost, write(postJsonBody)) @@ -281,9 +288,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit allPsd2 = None ), recurringIndicator = true, - validUntil = "2020-12-31", + validUntil = getNextMonthDate(), frequencyPerDay = 4, - combinedServiceIndicator = false + combinedServiceIndicator = Some(false) ) val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) val response: APIResponse = makePostRequest(requestPost, write(postJsonBody)) @@ -326,9 +333,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit allPsd2 = None ), recurringIndicator = true, - validUntil = "2020-12-31", + validUntil = getNextMonthDate(), frequencyPerDay = 4, - combinedServiceIndicator = false + combinedServiceIndicator = Some(false) ) val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) val response: APIResponse = makePostRequest(requestPost, write(postJsonBody)) @@ -374,9 +381,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit allPsd2 = None ), recurringIndicator = true, - validUntil = "2020-12-31", + validUntil = getNextMonthDate(), frequencyPerDay = 4, - combinedServiceIndicator = false + combinedServiceIndicator = Some(false) ) val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) val response: APIResponse = makePostRequest(requestPost, write(postJsonBody)) @@ -432,9 +439,9 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit allPsd2 = None ), recurringIndicator = true, - validUntil = "2020-12-31", + validUntil = getNextMonthDate(), frequencyPerDay = 4, - combinedServiceIndicator = false + combinedServiceIndicator = Some(false) ) val requestPost = (V1_3_BG / "consents" ).POST <@ (user1) val response: APIResponse = makePostRequest(requestPost, write(postJsonBody))