Merge pull request #2507 from constantine2nd/develop

Berlin Group
This commit is contained in:
Simon Redfern 2025-03-17 18:11:56 +01:00 committed by GitHub
commit f3177219c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 114 additions and 528 deletions

View File

@ -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

View File

@ -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"
)

View File

@ -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"

View File

@ -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)
}
}
}
}

View File

@ -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
)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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.")
}

View File

@ -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 {

View File

@ -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")
}
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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))

View File

@ -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!"

View File

@ -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"
}

View File

@ -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]
}
}
}

View File

@ -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))