From 937f3e914f1e7f69ddad75940adc8b7841023acf Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 08:08:21 +0200 Subject: [PATCH 1/8] v5.0.0 retirement (1/3): nameOf cosmetic fixes in Http4s500 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three resource docs used hardcoded name strings ("createBank", "updateBank", "createAccount") instead of nameOf(...). The literal value equals the val name in each case, so this is behaviour-neutral — it brings the file in line with the other 39 resource docs in Http4s500 and matches the cosmetic-fix pre-condition used in the v5.1.0 retirement (commit 88f46f854). Pre-condition for the upcoming OBPAPI5_0_0 rewire and APIMethods500 stub. --- obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala index 794ce680ed..d0254162d9 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala @@ -479,7 +479,7 @@ object Http4s500 { } resourceDocs += ResourceDoc( - null, implementedInApiVersion, "createBank", "POST", + null, implementedInApiVersion, nameOf(createBank), "POST", "/banks", "Create Bank", s"""Create a new bank (Authenticated access). | @@ -535,7 +535,7 @@ object Http4s500 { } resourceDocs += ResourceDoc( - null, implementedInApiVersion, "updateBank", "PUT", + null, implementedInApiVersion, nameOf(updateBank), "PUT", "/banks", "Update Bank", "Update an existing bank (Authenticated access).", postBankJson500, bankJson500, @@ -615,7 +615,7 @@ object Http4s500 { } resourceDocs += ResourceDoc( - null, implementedInApiVersion, "createAccount", "PUT", + null, implementedInApiVersion, nameOf(createAccount), "PUT", "/banks/BANK_ID/accounts/NEW_ACCOUNT_ID", "Create Account (PUT)", """Create Account at bank specified by BANK_ID with Id specified by ACCOUNT_ID. | From b7788e83fb56a7f433a5795cd557b0a92ce0e3f7 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 08:10:48 +0200 Subject: [PATCH 2/8] v5.0.0 retirement (2/3): rewire OBPAPI5_0_0 to Http4s500, drop Lift routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v500ToV400Bridge and v6→v5.1→v5.0 cascade are already operational, and Http4s500 has all 41 v5.0.0-native endpoints with 42 resourceDocs. This commit removes the parallel Lift dispatch path: - OBPAPI5_0_0: drop all `with APIMethods130 … with APIMethods500` mixins, drop `endpointsOf5_0_0` and `getAllowedEndpoints(...)`. routes=Nil. allResourceDocs now aggregates Http4s500.resourceDocs (42 docs) on top of OBPAPI4_0_0.allResourceDocs, replacing the previous reference to APIMethods500's inner-class Implementations5_0_0.resourceDocs (37 docs). The Lift CORS this.serve(...) block is removed — http4s corsHandler handles OPTIONS preflight. Re-exports `Implementations5_0_0 = Http4s500.Implementations5_0_0` to keep test imports compiling after APIMethods500 is stubbed in the next commit. - ResourceDocsAPIMethods: add `case ApiVersion.v5_0_0 => resourceDocs` to the skip filter. Without this, /resource-docs/v5.0.0/obp would filter the aggregated docs by Lift route class (now empty) and return nothing. After this commit APIMethods500 is dead code — no longer mixed in or referenced. Behaviour is unchanged: all v5.0.0 requests already routed through Http4s500 before this commit; only the (unused) Lift fallback path is removed. --- .../ResourceDocsAPIMethods.scala | 1 + .../scala/code/api/v5_0_0/OBPAPI5_0_0.scala | 80 ++++--------------- 2 files changed, 18 insertions(+), 63 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 8732e2f928..4925b614dc 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -179,6 +179,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth case ApiVersion.v1_2_1 => resourceDocs case ApiVersion.v6_0_0 => resourceDocs // fully on http4s — no Lift route filter case ApiVersion.v5_1_0 => resourceDocs // fully on http4s — no Lift route filter + case ApiVersion.v5_0_0 => resourceDocs // fully on http4s — no Lift route filter case _ => resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass)) } diff --git a/obp-api/src/main/scala/code/api/v5_0_0/OBPAPI5_0_0.scala b/obp-api/src/main/scala/code/api/v5_0_0/OBPAPI5_0_0.scala index ac3528d8d6..772a2a776d 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/OBPAPI5_0_0.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/OBPAPI5_0_0.scala @@ -28,83 +28,37 @@ package code.api.v5_0_0 import scala.language.reflectiveCalls import code.api.OBPRestHelper -import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints} -import code.api.util.{APIUtil, VersionedOBPApis} -import code.api.v1_3_0.APIMethods130 -import code.api.v1_4_0.APIMethods140 -import code.api.v2_0_0.APIMethods200 -import code.api.v2_1_0.APIMethods210 -import code.api.v2_2_0.APIMethods220 -import code.api.v3_0_0.APIMethods300 -import code.api.v3_0_0.custom.CustomAPIMethods300 -import code.api.v3_1_0.{APIMethods310, OBPAPI3_1_0} -import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0} -import code.api.v4_0_0.OBPAPI4_0_0.{Implementations4_0_0, endpointsOf4_0_0} +import code.api.util.APIUtil.OBPEndpoint +import code.api.util.VersionedOBPApis +import code.api.v4_0_0.OBPAPI4_0_0 import code.util.Helper.MdcLoggable -import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus} -import net.liftweb.common.{Box, Full} -import net.liftweb.http.{LiftResponse, PlainTextResponse} -import org.apache.http.HttpStatus /* -This file defines which endpoints from all the versions are available in v5.0.0 +This file defines which endpoints from all the versions are available in v5.0.0. +All v5.0.0 endpoints have been migrated to Http4s500 — this object is retained +only for resource-doc aggregation and the Lift dispatch registry. */ -object OBPAPI5_0_0 extends OBPRestHelper - with APIMethods130 - with APIMethods140 - with APIMethods200 - with APIMethods210 - with APIMethods220 - with APIMethods300 - with CustomAPIMethods300 - with APIMethods310 - with APIMethods400 - with APIMethods500 - with MdcLoggable - with VersionedOBPApis{ +object OBPAPI5_0_0 extends OBPRestHelper with MdcLoggable with VersionedOBPApis { - val version : ApiVersion = ApiVersion.v5_0_0 + val version: ApiVersion = ApiVersion.v5_0_0 + val versionStatus = ApiVersionStatus.STABLE.toString - val versionStatus = ApiVersionStatus.STABLE.toString + // Re-export so tests that import OBPAPI5_0_0.Implementations5_0_0 still compile + // after APIMethods500 is replaced with an empty stub. + val Implementations5_0_0 = Http4s500.Implementations5_0_0 - // Possible Endpoints from 5.0.0, exclude one endpoint use - method,exclude multiple endpoints use -- method, - // e.g getEndpoints(Implementations5_0_0) -- List(Implementations5_0_0.genericEndpoint, Implementations5_0_0.root) - lazy val endpointsOf5_0_0 = getEndpoints(Implementations5_0_0) - - // if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc. + // All v5.0.0 endpoints live in Http4s500 — aggregate Http4s500.resourceDocs on top of v4.0.0. def allResourceDocs = collectResourceDocs( OBPAPI4_0_0.allResourceDocs, - Implementations5_0_0.resourceDocs + Http4s500.resourceDocs ) - // all endpoints - private val endpoints: List[OBPEndpoint] = OBPAPI4_0_0.routes ++ endpointsOf5_0_0 - - // Filter the possible endpoints by the disabled / enabled Props settings and add them together - val routes : List[OBPEndpoint] = getAllowedEndpoints(endpoints, allResourceDocs) + // No Lift routes — all v5.0.0 endpoints are served by Http4s500. + val routes: List[OBPEndpoint] = Nil - // register v5.0.0 apis first, Make them available for use! registerRoutes(routes, allResourceDocs, apiPrefix, true) - logger.info(s"version $version has been run! There are ${routes.length} routes, ${allResourceDocs.length} allResourceDocs.") - - // specified response for OPTIONS request. - private val corsResponse: Box[LiftResponse] = Full{ - val corsHeaders = List( - "Access-Control-Allow-Origin" -> "*", - "Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE", - "Access-Control-Allow-Headers" -> "*", - "Access-Control-Allow-Credentials" -> "true", - "Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days - ) - PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT) - } - /* - * process OPTIONS http request, just return no content and status is 204 - */ - this.serve({ - case req if req.requestType.method == "OPTIONS" => corsResponse - }) + // CORS for OPTIONS is handled by the http4s corsHandler layer — no Lift serve needed here. } From 8858fd091dd445b75d71503d86e1629eb9bef6e9 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 08:15:42 +0200 Subject: [PATCH 3/8] v5.0.0 retirement (3/3): replace APIMethods500 with thin stub All v5.0.0 Lift endpoints live in Http4s500. APIMethods500 is dead code. Replace the 2,524-line implementation with an empty trait stub + re-export object so any existing import of APIMethods500.Implementations5_0_0 still resolves (delegates to Http4s500.Implementations5_0_0). Original code kept as block comments for grep-ability. --- .../scala/code/api/v5_0_0/APIMethods500.scala | 5054 +++++++++-------- 1 file changed, 2534 insertions(+), 2520 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala index cd4389cf65..d279c6350a 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala @@ -1,2524 +1,2538 @@ package code.api.v5_0_0 -import scala.language.reflectiveCalls -import code.accountattribute.AccountAttributeX -import code.api.Constant._ -import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ -import code.api.util.APIUtil._ -import code.api.util.ApiRole._ -import code.api.util.ApiTag._ -import code.api.util.ErrorMessages._ -import code.api.util.FutureUtil.EndpointContext -import code.api.util.NewStyle.HttpCode -import code.api.util.NewStyle.function.extractQueryParams -import code.api.util._ -import code.api.util.newstyle.ViewNewStyle -import code.api.v2_1_0.JSONFactory210 -import code.api.v3_0_0.JSONFactory300 -import code.api.v3_1_0._ -import code.api.v4_0_0.JSONFactory400.createCustomersMinimalJson -import code.api.v4_0_0.{JSONFactory400, PostCounterpartyJson400} -import code.api.v5_0_0.JSONFactory500.{createPhysicalCardJson, createViewJsonV500, createViewsIdsJsonV500, createViewsJsonV500} -import code.api.v5_1_0.{CreateCustomViewJson, PostCounterpartyLimitV510, PostVRPConsentRequestJsonV510} -import code.bankconnectors.Connector -import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent} -import code.consumer.Consumers -import code.entitlement.Entitlement -import code.metadata.counterparties.MappedCounterparty -import code.metrics.APIMetrics -import code.model.dataAccess.BankAccountCreation -import code.util.Helper -import code.util.Helper.{SILENCE_IS_GOLDEN, booleanToFuture} -import code.views.Views -import com.github.dwickern.macros.NameOf.nameOf -import com.openbankproject.commons.ExecutionContext.Implicits.global -import com.openbankproject.commons.model._ -import com.openbankproject.commons.model.enums.StrongCustomerAuthentication -import com.openbankproject.commons.util.ApiVersion -import net.liftweb.common.{Empty, Full} -import net.liftweb.http.Req -import net.liftweb.http.rest.RestHelper -import net.liftweb.json -import net.liftweb.json.{Extraction, compactRender, prettyRender} -import net.liftweb.mapper.By -import net.liftweb.util.Helpers.tryo -import net.liftweb.util.{Helpers, Props, StringHelpers} - -import java.util.UUID -import java.util.concurrent.ThreadLocalRandom -import scala.collection.immutable.{List, Nil} -import scala.collection.mutable.ArrayBuffer -import scala.concurrent.Future -import scala.util.Random - -trait APIMethods500 { - self: RestHelper => - - val Implementations5_0_0 = new Implementations500() - - protected trait TestHead { - /** - * Test to see if the request is a GET and expecting JSON in the response. - * The path and the Req instance are extracted. - */ - def unapply(r: Req): Option[(List[String], Req)] = - if (r.requestType.head_? && testResponse_?(r)) - Some(r.path.partPath -> r) else None - - def testResponse_?(r: Req): Boolean - } - - lazy val JsonHead = new TestHead with JsonTest - - class Implementations500 { - - val implementedInApiVersion = ApiVersion.v5_0_0 - - private val staticResourceDocs = ArrayBuffer[ResourceDoc]() - def resourceDocs = staticResourceDocs - - val apiRelations = ArrayBuffer[ApiRelation]() - val codeContext = CodeContext(staticResourceDocs, apiRelations) - - - staticResourceDocs += ResourceDoc( - root, - implementedInApiVersion, - "root", - "GET", - "/root", - "Get API Info (root)", - """Returns information about: - | - |* API version - |* Hosted by information - |* Hosted at information - |* Energy source information - |* Git Commit""", - EmptyBody, - apiInfoJson400, - List(UnknownError, MandatoryPropertyIsNotSet), - apiTagApi :: Nil) - - lazy val root: OBPEndpoint = { - case (Nil | "root" :: Nil) JsonGet _ => { - cc => - implicit val ec = EndpointContext(Some(cc)) - for { - _ <- Future(()) // Just start async call - } yield { - (JSONFactory400.getApiInfoJSON(OBPAPI5_0_0.version,OBPAPI5_0_0.versionStatus), HttpCode.`200`(cc.callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getBank, - implementedInApiVersion, - nameOf(getBank), - "GET", - "/banks/BANK_ID", - "Get Bank", - """Get the bank specified by BANK_ID - |Returns information about a single bank specified by BANK_ID including: - | - |* Bank code and full name of bank - |* Logo URL - |* Website""", - EmptyBody, - bankJson500, - List(UnknownError, BankNotFound), - apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil - ) - - lazy val getBank : OBPEndpoint = { - case "banks" :: BankId(bankId) :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (bank, callContext) <- NewStyle.function.getBank(bankId, cc.callContext) - (attributes, callContext) <- NewStyle.function.getBankAttributesByBank(bankId, callContext) - } yield - (JSONFactory500.createBankJSON500(bank, attributes), HttpCode.`200`(callContext)) - } - } - - staticResourceDocs += ResourceDoc( - createBank, - implementedInApiVersion, - "createBank", - "POST", - "/banks", - "Create Bank", - s"""Create a new bank (Authenticated access). - | - |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. - |Thus the User can manage the bank they create and assign Roles to other Users. - | - |Only SANDBOX mode (i.e. when connector=mapped in properties file) - |The settlement accounts are automatically created by the system when the bank is created. - |Name and account id are created in accordance to the next rules: - | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) - | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) - | - |""", - postBankJson500, - bankJson500, - List( - InvalidJsonFormat, - $AuthenticatedUserIsRequired, - InsufficientAuthorisationToCreateBank, - UnknownError - ), - List(apiTagBank), - Some(List(canCreateBank)) - ) - - lazy val createBank: OBPEndpoint = { - case "banks" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson500 " - for { - postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { - json.extract[PostBankJson500] - } - - //if postJson.id is empty, just return SILENCE_IS_GOLDEN, and will pass the guard. - checkShortStringValue = APIUtil.checkOptionalShortString(postJson.id.getOrElse(SILENCE_IS_GOLDEN)) - _ <- Helper.booleanToFuture(failMsg = s"$checkShortStringValue.", cc = cc.callContext) { - checkShortStringValue == SILENCE_IS_GOLDEN - } - - _ <- Helper.booleanToFuture(failMsg = ErrorMessages.InvalidConsumerCredentials, cc=cc.callContext) { - cc.callContext.map(_.consumer.isDefined == true).isDefined - } - _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat Min length of BANK_ID should be greater than 3 characters.", cc=cc.callContext) { - postJson.id.forall(_.length > 3) - } - _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain space characters", cc=cc.callContext) { - !postJson.id.contains(" ") - } - _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain `::::` characters", cc=cc.callContext) { - !`checkIfContains::::`(postJson.id.getOrElse("")) - } - (banks, callContext) <- NewStyle.function.getBanks(cc.callContext) - _ <- Helper.booleanToFuture(failMsg = ErrorMessages.bankIdAlreadyExists, cc=cc.callContext) { - !banks.exists { b => Some(b.bankId.value) == postJson.id } - } - (success, callContext) <- NewStyle.function.createOrUpdateBank( - postJson.id.getOrElse(APIUtil.generateUUID()), - postJson.full_name.getOrElse(""), - postJson.bank_code, - postJson.logo.getOrElse(""), - postJson.website.getOrElse(""), - postJson.bank_routings.getOrElse(Nil).find(_.scheme == "BIC").map(_.address).getOrElse(""), - "", - postJson.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.scheme).getOrElse(""), - postJson.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.address).getOrElse(""), - callContext - ) - entitlements <- NewStyle.function.getEntitlementsByUserId(cc.userId, callContext) - entitlementsByBank = entitlements.filter(_.bankId==postJson.id.getOrElse("")) - _ <- entitlementsByBank.filter(_.roleName == CanCreateEntitlementAtOneBank.toString()).size > 0 match { - case true => - // Already has entitlement - Future(()) - case false => - Future(Entitlement.entitlement.vend.addEntitlement(postJson.id.getOrElse(""), cc.userId, CanCreateEntitlementAtOneBank.toString())) - } - _ <- entitlementsByBank.filter(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()).size > 0 match { - case true => - // Already has entitlement - Future(()) - case false => - Future(Entitlement.entitlement.vend.addEntitlement(postJson.id.getOrElse(""), cc.userId, CanReadDynamicResourceDocsAtOneBank.toString())) - } - } yield { - (JSONFactory500.createBankJSON500(success), HttpCode.`201`(callContext)) - } - } - } - staticResourceDocs += ResourceDoc( - updateBank, - implementedInApiVersion, - "updateBank", - "PUT", - "/banks", - "Update Bank", - s"""Update an existing bank (Authenticated access). - | - |""", - postBankJson500, - bankJson500, - List( - InvalidJsonFormat, - $AuthenticatedUserIsRequired, - BankNotFound, - updateBankError, - UnknownError - ), - List(apiTagBank), - Some(List(canCreateBank)) - ) - - lazy val updateBank: OBPEndpoint = { - case "banks" :: Nil JsonPut json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson500 " - for { - bank <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { - json.extract[PostBankJson500] - } - _ <- Helper.booleanToFuture(failMsg = ErrorMessages.InvalidConsumerCredentials, cc=cc.callContext) { - cc.callContext.map(_.consumer.isDefined == true).isDefined - } - _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat Min length of BANK_ID should be greater than 3 characters.", cc=cc.callContext) { - bank.id.forall(_.length > 3) - } - _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain space characters", cc=cc.callContext) { - !bank.id.contains(" ") - } - bankId <- NewStyle.function.tryons(ErrorMessages.updateBankError, 400, cc.callContext) { - bank.id.get - } - (_, callContext) <- NewStyle.function.getBank(BankId(bankId), cc.callContext) - (success, callContext) <- NewStyle.function.createOrUpdateBank( - bankId, - bank.full_name.getOrElse(""), - bank.bank_code, - bank.logo.getOrElse(""), - bank.website.getOrElse(""), - bank.bank_routings.getOrElse(Nil).find(_.scheme == "BIC").map(_.address).getOrElse(""), - "", - bank.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.scheme).getOrElse(""), - bank.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.address).getOrElse(""), - callContext - ) - } yield { - (JSONFactory500.createBankJSON500(success), HttpCode.`200`(callContext)) - } - } - } - - - resourceDocs += ResourceDoc( - createAccount, - implementedInApiVersion, - "createAccount", - "PUT", - "/banks/BANK_ID/accounts/ACCOUNT_ID", - "Create Account (PUT)", - """Create Account at bank specified by BANK_ID with Id specified by ACCOUNT_ID. - | - |The User can create an Account for themself - or - the User that has the USER_ID specified in the POST body. - | - |If the PUT body USER_ID *is* specified, the logged in user must have the Role canCreateAccount. Once created, the Account will be owned by the User specified by USER_ID. - | - |If the PUT body USER_ID is *not* specified, the account will be owned by the logged in User. - | - |The 'product_code' field SHOULD be a product_code from Product. - |If the 'product_code' matches a product_code from Product, account attributes will be created that match the Product Attributes. - | - |Note: The Amount MUST be zero.""".stripMargin, - createAccountRequestJsonV500, - createAccountResponseJsonV310, - List( - InvalidJsonFormat, - BankNotFound, - AuthenticatedUserIsRequired, - InvalidUserId, - InvalidAccountIdFormat, - InvalidBankIdFormat, - UserNotFoundById, - UserHasMissingRoles, - InvalidAccountBalanceAmount, - InvalidAccountInitialBalance, - InitialBalanceMustBeZero, - InvalidAccountBalanceCurrency, - AccountIdAlreadyExists, - UnknownError - ), - List(apiTagAccount,apiTagOnboarding), - Some(List(canCreateAccount)) - ) - - - lazy val createAccount : OBPEndpoint = { - // Create a new account - case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonPut json -> _ => { - cc =>{ - implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), callContext) <- authenticatedAccess(cc) - (account, callContext) <- Connector.connector.vend.checkBankAccountExists(bankId, accountId, callContext) - _ <- Helper.booleanToFuture(AccountIdAlreadyExists, cc=callContext){ - account.isEmpty - } - failMsg = s"$InvalidJsonFormat The Json body should be the ${prettyRender(Extraction.decompose(createAccountRequestJsonV310))} " - createAccountJson <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[CreateAccountRequestJsonV500] - } - loggedInUserId = u.userId - userIdAccountOwner = createAccountJson.user_id match { - case Some(userId) => userId - case _ => loggedInUserId - } - _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext){ - isValidID(accountId.value) - } - _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext){ - isValidID(accountId.value) - } - (postedOrLoggedInUser,callContext) <- NewStyle.function.findByUserId(userIdAccountOwner, callContext) - // User can create account for self or an account for another user if they have CanCreateAccount role - _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext){ - isValidID(accountId.value) - } - _ <- {userIdAccountOwner == loggedInUserId} match { - case true => Future.successful(Full(Unit)) - case false => - NewStyle.function.hasEntitlement( - bankId.value, - loggedInUserId, - canCreateAccount, - callContext, - s"${UserHasMissingRoles} $canCreateAccount or create account for self" - ) - } - initialBalanceAsString = createAccountJson.balance.map(_.amount).getOrElse("0") - accountType = createAccountJson.product_code - accountLabel = createAccountJson.label - initialBalanceAsNumber <- NewStyle.function.tryons(InvalidAccountInitialBalance, 400, callContext) { - BigDecimal(initialBalanceAsString) - } - _ <- Helper.booleanToFuture(InitialBalanceMustBeZero, cc=callContext){0 == initialBalanceAsNumber} - _ <- Helper.booleanToFuture(InvalidISOCurrencyCode, cc=callContext){isValidCurrencyISOCode(createAccountJson.balance.map(_.currency).getOrElse("EUR"))} - currency = createAccountJson.balance.map(_.currency).getOrElse("EUR") - (_, callContext ) <- NewStyle.function.getBank(bankId, callContext) - _ <- Helper.booleanToFuture(s"$InvalidAccountRoutings Duplication detected in account routings, please specify only one value per routing scheme", 400, cc=callContext){ - createAccountJson.account_routings.getOrElse(Nil).map(_.scheme).distinct.size == createAccountJson.account_routings.getOrElse(Nil).size - } - alreadyExistAccountRoutings <- Future.sequence(createAccountJson.account_routings.getOrElse(Nil).map(accountRouting => - NewStyle.function.getAccountRouting(Some(bankId), accountRouting.scheme, accountRouting.address, callContext).map(_ => Some(accountRouting)).fallbackTo(Future.successful(None)) - )) - alreadyExistingAccountRouting = alreadyExistAccountRoutings.collect { - case Some(accountRouting) => s"bankId: $bankId, scheme: ${accountRouting.scheme}, address: ${accountRouting.address}" - } - _ <- Helper.booleanToFuture(s"$AccountRoutingAlreadyExist (${alreadyExistingAccountRouting.mkString("; ")})", cc=callContext) { - alreadyExistingAccountRouting.isEmpty - } - (bankAccount,callContext) <- NewStyle.function.createBankAccount( - bankId, - accountId, - accountType, - accountLabel, - currency, - initialBalanceAsNumber, - postedOrLoggedInUser.name, - createAccountJson.branch_id.getOrElse(""), - createAccountJson.account_routings.getOrElse(Nil).map(r => AccountRouting(r.scheme, r.address)), - callContext - ) - (productAttributes, callContext) <- NewStyle.function.getProductAttributesByBankAndCode(bankId, ProductCode(accountType), callContext) - (accountAttributes, callContext) <- NewStyle.function.createAccountAttributes( - bankId, - accountId, - ProductCode(accountType), - productAttributes, - None, - callContext: Option[CallContext] - ) - //1 Create or Update the `Owner` for the new account - //2 Add permission to the user - //3 Set the user as the account holder - _ <- BankAccountCreation.setAccountHolderAndRefreshUserAccountAccess(bankId, accountId, postedOrLoggedInUser, callContext) - } yield { - (JSONFactory310.createAccountJSON(userIdAccountOwner, bankAccount, accountAttributes), HttpCode.`201`(callContext)) - } - } - } - } - - - staticResourceDocs += ResourceDoc( - createUserAuthContext, - implementedInApiVersion, - nameOf(createUserAuthContext), - "POST", - "/users/USER_ID/auth-context", - "Create User Auth Context", - s"""Create User Auth Context. These key value pairs will be propagated over connector to adapter. Normally used for mapping OBP user and - | Bank User/Customer. - |${userAuthenticationMessage(true)} - |""", - postUserAuthContextJson, - userAuthContextJsonV500, - List( - AuthenticatedUserIsRequired, - InvalidJsonFormat, - CreateUserAuthContextError, - UnknownError - ), - List(apiTagUser), - Some(List(canCreateUserAuthContext))) - lazy val createUserAuthContext : OBPEndpoint = { - case "users" :: userId ::"auth-context" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateUserAuthContext, callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextJson " - postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[PostUserAuthContextJson] - } - (user, callContext) <- NewStyle.function.findByUserId(userId, callContext) - (userAuthContext, callContext) <- NewStyle.function.createUserAuthContext(user, postedData.key.trim, postedData.value.trim, callContext) - } yield { - (JSONFactory500.createUserAuthContextJson(userAuthContext), HttpCode.`201`(callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - getUserAuthContexts, - implementedInApiVersion, - nameOf(getUserAuthContexts), - "GET", - "/users/USER_ID/auth-context", - "Get User Auth Contexts", - s"""Get User Auth Contexts for a User. - | - | - |${userAuthenticationMessage(true)} - | - |""", - EmptyBody, - userAuthContextJsonV500, - List( - AuthenticatedUserIsRequired, - UserHasMissingRoles, - UnknownError - ), - List(apiTagUser), - Some(canGetUserAuthContext :: Nil) - ) - lazy val getUserAuthContexts : OBPEndpoint = { - case "users" :: userId :: "auth-context" :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, canGetUserAuthContext, callContext) - (_, callContext) <- NewStyle.function.findByUserId(userId, callContext) - (userAuthContexts, callContext) <- NewStyle.function.getUserAuthContexts(userId, callContext) - } yield { - (JSONFactory500.createUserAuthContextsJson(userAuthContexts), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - createUserAuthContextUpdateRequest, - implementedInApiVersion, - nameOf(createUserAuthContextUpdateRequest), - "POST", - "/banks/BANK_ID/users/current/auth-context-updates/SCA_METHOD", - "Create User Auth Context Update Request", - s"""Create User Auth Context Update Request. - |${userAuthenticationMessage(true)} - | - |A One Time Password (OTP) (AKA security challenge) is sent Out of Band (OOB) to the User via the transport defined in SCA_METHOD - |SCA_METHOD is typically "SMS" or "EMAIL". "EMAIL" is used for testing purposes. - | - |""", - postUserAuthContextJson, - userAuthContextUpdateJsonV500, - List( - AuthenticatedUserIsRequired, - $BankNotFound, - InvalidJsonFormat, - CreateUserAuthContextError, - UnknownError - ), - List(apiTagUser), - None - ) - - lazy val createUserAuthContextUpdateRequest : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "users" :: "current" ::"auth-context-updates" :: scaMethod :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(user), callContext) <- authenticatedAccess(cc) - _ <- Helper.booleanToFuture(failMsg = ConsumerHasMissingRoles + CanCreateUserAuthContextUpdate, cc=callContext) { - checkScope(bankId.value, getConsumerPrimaryKey(callContext), ApiRole.canCreateUserAuthContextUpdate) - } - _ <- Helper.booleanToFuture(UserAuthContextUpdateRequestAllowedScaMethods, cc=callContext){ - List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString()).exists(_ == scaMethod) - } - failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextJson " - postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[PostUserAuthContextJson] - } - (userAuthContextUpdate, callContext) <- NewStyle.function.validateUserAuthContextUpdateRequest(bankId.value, user.userId, postedData.key.trim, postedData.value.trim, scaMethod, callContext) - } yield { - - (JSONFactory500.createUserAuthContextUpdateJson(userAuthContextUpdate), HttpCode.`201`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - answerUserAuthContextUpdateChallenge, - implementedInApiVersion, - nameOf(answerUserAuthContextUpdateChallenge), - "POST", - "/banks/BANK_ID/users/current/auth-context-updates/AUTH_CONTEXT_UPDATE_ID/challenge", - "Answer User Auth Context Update Challenge", - s""" - |Answer User Auth Context Update Challenge. - |""", - postUserAuthContextUpdateJsonV310, - userAuthContextUpdateJsonV500, - List( - AuthenticatedUserIsRequired, - $BankNotFound, - InvalidJsonFormat, - InvalidConnectorResponse, - UnknownError - ), - apiTagUser :: Nil) - - lazy val answerUserAuthContextUpdateChallenge : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "users" :: "current" ::"auth-context-updates" :: authContextUpdateId :: "challenge" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, callContext) <- authenticatedAccess(cc) - failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextUpdateJsonV310 " - postUserAuthContextUpdateJson <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[PostUserAuthContextUpdateJsonV310] - } - (userAuthContextUpdate, callContext) <- NewStyle.function.checkAnswer(authContextUpdateId, postUserAuthContextUpdateJson.answer, callContext) - (user, callContext) <- NewStyle.function.getUserByUserId(userAuthContextUpdate.userId, callContext) - (_, callContext) <- - userAuthContextUpdate.status match { - case status if status == UserAuthContextUpdateStatus.ACCEPTED.toString => - NewStyle.function.createUserAuthContext( - user, - userAuthContextUpdate.key.trim, - userAuthContextUpdate.value.trim, - callContext).map(x => (Some(x._1), x._2)) - case _ => - Future((None, callContext)) - } - (_, callContext) <- - userAuthContextUpdate.key match { - case "CUSTOMER_NUMBER" => - NewStyle.function.getOCreateUserCustomerLink( - bankId, - userAuthContextUpdate.value, // Customer number - user.userId, - callContext - ) - case _ => - Future((None, callContext)) - } - } yield { - (JSONFactory500.createUserAuthContextUpdateJson(userAuthContextUpdate), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - createConsentRequest, - implementedInApiVersion, - nameOf(createConsentRequest), - "POST", - "/consumer/consent-requests", - "Create Consent Request", - s""" - |Create a Consent Request — the first step of the OBP Consent flow. - | - |The calling application (TPP) authenticates with Client Credentials and posts the consent details (entitlements, account_access, VRP fields, etc.). The User then completes the flow by calling Create Consent By CONSENT_REQUEST_ID. - | - |The Consent Request is recorded against the calling consumer (its creator). - | - |Optional body fields of note: - | - |- `consumer_id`: if set, the resulting Consent (created when the User answers this request) will be pinned to this consumer instead of the creator. Use only when the consent is intended for a different application than the one creating the request. Most TPPs should omit this field — when omitted, the resulting Consent is pinned to the creator. Note: this override is being deprecated; v6.0.0 will pin the resulting Consent to the creator unconditionally. - |- `email` / `phone_number`: surface in the SCA challenge if the User chooses EMAIL or SMS at answer time. - |- `valid_from` / `time_to_live`: control the lifetime of the resulting Consent. - | - |Authentication: - | - |The client needs to authenticate themselves for this request. In case of public client we use client_id and private key to obtain access token; otherwise we use client_id and client_secret. The obtained access token is used in the HTTP Bearer auth header of our request. - | - |Example: - |Authorization: Bearer eXtneO-THbQtn3zvK_kQtXXfvOZyZFdBCItlPDbR2Bk.dOWqtXCtFX-tqGTVR0YrIjvAolPIVg7GZ-jz83y6nA0 - | - |After successfully creating the Consent Request, call Create Consent By CONSENT_REQUEST_ID to finalize. - | - |See Glossary entry "Authentication: Consent OBP Flow Example" for an end-to-end walk-through. - | - |${applicationAccessMessage(true)} - | - |${userAuthenticationMessage(false)} - | - | - |""".stripMargin, - postConsentRequestJsonV500, - consentRequestResponseJson, - List( - InvalidJsonFormat, - ConsentMaxTTL, - X509CannotGetCertificate, - X509GeneralError, - InvalidConnectorResponse, - UnknownError - ), - apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil - ) - - lazy val createConsentRequest : OBPEndpoint = { - case "consumer" :: "consent-requests" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, callContext) <- applicationAccess(cc) - _ <- passesPsd2Aisp(callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $PostConsentBodyCommonJson " - consentJson: PostConsentRequestJsonV500 <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[PostConsentRequestJsonV500] - } - maxTimeToLive = APIUtil.getPropsAsIntValue(nameOfProperty="consents.max_time_to_live", defaultValue=3600) - _ <- Helper.booleanToFuture(s"$ConsentMaxTTL ($maxTimeToLive)", cc=callContext){ - consentJson.time_to_live match { - case Some(ttl) => ttl <= maxTimeToLive - case _ => true - } - } - createdConsentRequest <- Future(ConsentRequests.consentRequestProvider.vend.createConsentRequest( - callContext.flatMap(_.consumer), - Some(compactRender(json)) - )) map { - i => connectorEmptyResponse(i, callContext) - } - } yield { - (JSONFactory500.createConsentRequestResponseJson(createdConsentRequest), HttpCode.`201`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getConsentRequest, - implementedInApiVersion, - nameOf(getConsentRequest), - "GET", - "/consumer/consent-requests/CONSENT_REQUEST_ID", - "Get Consent Request", - s""" - |Return the full payload of a previously-created Consent Request — the JSON the TPP submitted to Create Consent Request, plus the consent_request_id and the creating consumer_id. - | - |Use this endpoint to verify the contents of a Consent Request before the User answers it. - | - |Authentication: Application access (any registered consumer/application can read any Consent Request by ID). - | - |Note: this endpoint will be restricted to the creating consumer in v6.0.0. Until then, treat CONSENT_REQUEST_IDs as sensitive — they reveal entitlements, account routings, and contact details (email/phone) submitted at creation. - | - |""".stripMargin, - EmptyBody, - consentRequestResponseJson, - List( - InvalidJsonFormat, - ConsentMaxTTL, - X509CannotGetCertificate, - X509GeneralError, - InvalidConnectorResponse, - UnknownError - ), - apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil - ) - - lazy val getConsentRequest : OBPEndpoint = { - case "consumer" :: "consent-requests" :: consentRequestId :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, callContext) <- applicationAccess(cc) - _ <- passesPsd2Aisp(callContext) - createdConsentRequest <- Future(ConsentRequests.consentRequestProvider.vend.getConsentRequestById( - consentRequestId - )) map { - i => unboxFullOrFail(i,callContext, ConsentRequestNotFound) - } - } yield { - (JSONFactory500.createConsentRequestResponseJson(createdConsentRequest), HttpCode.`200`(callContext) - ) - } - } - } - - staticResourceDocs += ResourceDoc( - getConsentByConsentRequestId, - implementedInApiVersion, - nameOf(getConsentByConsentRequestId), - "GET", - "/consumer/consent-requests/CONSENT_REQUEST_ID/consents", - "Get Consent By Consent Request Id via Consumer", - s""" - | - |This endpoint gets the Consent By consent request id. - | - |${userAuthenticationMessage(true)} - | - """.stripMargin, - EmptyBody, - consentJsonV500, - List( - $AuthenticatedUserIsRequired, - UnknownError - ), - List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2)) - lazy val getConsentByConsentRequestId: OBPEndpoint = { - case "consumer" :: "consent-requests" :: consentRequestId :: "consents" :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, callContext) <- applicationAccess(cc) - consent <- Future { Consents.consentProvider.vend.getConsentByConsentRequestId(consentRequestId)} map { - unboxFullOrFail(_, callContext, ConsentRequestNotFound) - } - _ <- Helper.booleanToFuture(failMsg = ConsentNotFound, failCode = 404, cc = cc.callContext) { - consent.mConsumerId.get == cc.consumer.map(_.consumerId.get).getOrElse("None") - } - (bankId, accountId, viewId, helperInfo) <- NewStyle.function.tryons(failMsg = Oauth2BadJWTException, 400, callContext) { - val jsonWebTokenAsJValue = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(json.parse(_).extract[ConsentJWT]) - val viewsFromJwtToken = jsonWebTokenAsJValue.head.views - //at the moment,we only support VRP consent to show `ConsentAccountAccessJson` in the response. Because the TPP need them for payments. - val isVrpConsent = (viewsFromJwtToken.length == 1 )&& (viewsFromJwtToken.head.bank_id.nonEmpty)&& (viewsFromJwtToken.head.account_id.nonEmpty)&& (viewsFromJwtToken.head.view_id.startsWith("_vrp-")) - - if(isVrpConsent){ - val bankId = BankId(viewsFromJwtToken.head.bank_id) - val accountId = AccountId(viewsFromJwtToken.head.account_id) - val viewId = ViewId(viewsFromJwtToken.head.view_id) - val helperInfoFromJwtToken = viewsFromJwtToken.head.helper_info - val viewCanGetCounterparty = Views.views.vend.customView(viewId, BankIdAccountId(bankId, accountId)).map(_.allowed_actions.exists( _ == CAN_GET_COUNTERPARTY)) - val helperInfo = if(viewCanGetCounterparty==Full(true)) helperInfoFromJwtToken else None - (Some(bankId), Some(accountId), Some(viewId), helperInfo) - }else{ - (None, None, None, None) - } - } - } yield { - ( - ConsentJsonV500( - consent.consentId, - consent.jsonWebToken, - consent.status, - Some(consent.consentRequestId), - if (bankId.isDefined && accountId.isDefined && viewId.isDefined) { - Some(ConsentAccountAccessJson( - bank_id = bankId.get.value, - account_id = accountId.get.value, - view_id = viewId.get.value, - helper_info = helperInfo - )) - } else { - None - } - ), - HttpCode.`200`(cc) - ) - } - } - } - - staticResourceDocs += ResourceDoc( - createConsentByConsentRequestIdEmail, - implementedInApiVersion, - nameOf(createConsentByConsentRequestIdEmail), - "POST", - "/consumer/consent-requests/CONSENT_REQUEST_ID/EMAIL/consents", - "Create Consent By CONSENT_REQUEST_ID (EMAIL)", - s""" - |Answer a Consent Request and create the resulting Consent, with an EMAIL Strong Customer Authentication challenge. - | - |After the TPP has called Create Consent Request (Client Credentials), the User authenticates and answers the request via this endpoint. This creates the Consent (the credential the consumer will use to access OBP on the User's behalf). - | - |An SCA challenge code is sent to the email address that was supplied in the Create Consent Request body. The User then completes SCA via Answer Consent Challenge, which moves the Consent from INITIATED to ACCEPTED. - | - |Pinning: the resulting Consent is pinned to a single consumer at creation. The pinned consumer is taken from the `consumer_id` field of the original Create Consent Request body if present, otherwise from the consumer that created the Request. After creation, only that consumer can present the resulting Consent JWT — any other consumer presenting it gets ConsentNotFound (consumer mismatch). - | - |Each Consent Request can be answered exactly once. A second call returns ConsentRequestIsInvalid. - | - |The Consent's authority is bounded by the answering User's own entitlements — it cannot grant access beyond what that User already has. - | - |Authentication: Any authenticated User may answer a Consent Request whose CONSENT_REQUEST_ID they know. This will be tightened in v6.0.0; until then, treat CONSENT_REQUEST_IDs as sensitive. - | - |""", - EmptyBody, - consentJsonV500, - List( - AuthenticatedUserIsRequired, - BankNotFound, - InvalidJsonFormat, - ConsentAllowedScaMethods, - RolesAllowedInConsent, - ViewsAllowedInConsent, - ConsumerNotFoundByConsumerId, - ConsumerIsDisabled, - InvalidConnectorResponse, - UnknownError - ), - apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: apiTagVrp :: Nil) - - staticResourceDocs += ResourceDoc( - createConsentByConsentRequestIdSms, - implementedInApiVersion, - nameOf(createConsentByConsentRequestIdSms), - "POST", - "/consumer/consent-requests/CONSENT_REQUEST_ID/SMS/consents", - "Create Consent By CONSENT_REQUEST_ID (SMS)", - s""" - |Answer a Consent Request and create the resulting Consent, with an SMS Strong Customer Authentication challenge. - | - |After the TPP has called Create Consent Request (Client Credentials), the User authenticates and answers the request via this endpoint. This creates the Consent (the credential the consumer will use to access OBP on the User's behalf). - | - |An SCA challenge code is sent to the phone number that was supplied in the Create Consent Request body. The User then completes SCA via Answer Consent Challenge, which moves the Consent from INITIATED to ACCEPTED. - | - |Pinning: the resulting Consent is pinned to a single consumer at creation. The pinned consumer is taken from the `consumer_id` field of the original Create Consent Request body if present, otherwise from the consumer that created the Request. After creation, only that consumer can present the resulting Consent JWT — any other consumer presenting it gets ConsentNotFound (consumer mismatch). - | - |Each Consent Request can be answered exactly once. A second call returns ConsentRequestIsInvalid. - | - |The Consent's authority is bounded by the answering User's own entitlements — it cannot grant access beyond what that User already has. - | - |Authentication: Any authenticated User may answer a Consent Request whose CONSENT_REQUEST_ID they know. This will be tightened in v6.0.0; until then, treat CONSENT_REQUEST_IDs as sensitive. - | - |""", - EmptyBody, - consentJsonV500, - List( - AuthenticatedUserIsRequired, - $BankNotFound, - InvalidJsonFormat, - ConsentRequestIsInvalid, - ConsentAllowedScaMethods, - RolesAllowedInConsent, - ViewsAllowedInConsent, - ConsumerNotFoundByConsumerId, - ConsumerIsDisabled, - MissingPropsValueAtThisInstance, - SmsServerNotResponding, - InvalidConnectorResponse, - UnknownError - ), - apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil) - - staticResourceDocs += ResourceDoc( - createConsentByConsentRequestIdImplicit, - implementedInApiVersion, - nameOf(createConsentByConsentRequestIdImplicit), - "POST", - "/consumer/consent-requests/CONSENT_REQUEST_ID/IMPLICIT/consents", - "Create Consent By CONSENT_REQUEST_ID (IMPLICIT)", - s""" - |Answer a Consent Request and create the resulting Consent, without a Strong Customer Authentication challenge — the Consent is moved directly from INITIATED to ACCEPTED. - | - |After the TPP has called Create Consent Request (Client Credentials), the User authenticates and answers the request via this endpoint. This creates the Consent (the credential the consumer will use to access OBP on the User's behalf). - | - |IMPLICIT means no SCA challenge is sent. The Consent is immediately ACCEPTED. Use only in flows where the User has already been strongly authenticated by upstream means; for production use behind a public TPP, prefer EMAIL or SMS. - | - |Pinning: the resulting Consent is pinned to a single consumer at creation. The pinned consumer is taken from the `consumer_id` field of the original Create Consent Request body if present, otherwise from the consumer that created the Request. After creation, only that consumer can present the resulting Consent JWT — any other consumer presenting it gets ConsentNotFound (consumer mismatch). - | - |Each Consent Request can be answered exactly once. A second call returns ConsentRequestIsInvalid. - | - |The Consent's authority is bounded by the answering User's own entitlements — it cannot grant access beyond what that User already has. - | - |Authentication: Any authenticated User may answer a Consent Request whose CONSENT_REQUEST_ID they know. This will be tightened in v6.0.0; until then, treat CONSENT_REQUEST_IDs as sensitive. - | - |""", - EmptyBody, - consentJsonV500, - List( - AuthenticatedUserIsRequired, - $BankNotFound, - InvalidJsonFormat, - ConsentRequestIsInvalid, - ConsentAllowedScaMethods, - RolesAllowedInConsent, - ViewsAllowedInConsent, - ConsumerNotFoundByConsumerId, - ConsumerIsDisabled, - MissingPropsValueAtThisInstance, - SmsServerNotResponding, - InvalidConnectorResponse, - UnknownError - ), - apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil) - - lazy val createConsentByConsentRequestIdEmail = createConsentByConsentRequestId - lazy val createConsentByConsentRequestIdSms = createConsentByConsentRequestId - lazy val createConsentByConsentRequestIdImplicit = createConsentByConsentRequestId - - lazy val createConsentByConsentRequestId : OBPEndpoint = { - - case "consumer" :: "consent-requests":: consentRequestId :: scaMethod :: "consents" :: Nil JsonPost _ -> _ => { - def sendEmailNotification(callContext: Option[CallContext], consentRequestJson: PostConsentRequestJsonV500, challengeText: String) = { - for { - failMsg <- Future { - s"$InvalidJsonFormat The Json body must contain the field email" - } - consentScaEmail <- NewStyle.function.tryons(failMsg, 400, callContext) { - consentRequestJson.email.head - } - (status, callContext) <- NewStyle.function.sendCustomerNotification( - StrongCustomerAuthentication.EMAIL, - consentScaEmail, - Some("OBP Consent Challenge"), - challengeText, - callContext - ) - } yield { - status - } - } - def sendSmsNotification(callContext: Option[CallContext], consentRequestJson: PostConsentRequestJsonV500, challengeText: String) = { - for { - failMsg <- Future { - s"$InvalidJsonFormat The Json body must contain the field phone_number" - } - consentScaPhoneNumber <- NewStyle.function.tryons(failMsg, 400, callContext) { - consentRequestJson.phone_number.head - } - (status, callContext) <- NewStyle.function.sendCustomerNotification( - StrongCustomerAuthentication.SMS, - consentScaPhoneNumber, - None, - challengeText, - callContext - ) - } yield { - status - } - } - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(user), callContext) <- authenticatedAccess(cc) - createdConsentRequest <- Future(ConsentRequests.consentRequestProvider.vend.getConsentRequestById( - consentRequestId - )) map { - i => unboxFullOrFail(i,callContext, ConsentRequestNotFound) - } - _ <- Helper.booleanToFuture(s"$ConsentRequestIsInvalid, the current CONSENT_REQUEST_ID($consentRequestId) is already used to create a consent, please provide another one!", cc=callContext){ - Consents.consentProvider.vend.getConsentByConsentRequestId(consentRequestId).isEmpty - } - _ <- Helper.booleanToFuture(ConsentAllowedScaMethods, cc=callContext){ - List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString(), StrongCustomerAuthentication.IMPLICIT.toString()).exists(_ == scaMethod) - } - // If the payload contains "to_account` , it mean it is a VRP consent. - isVrpConsent = createdConsentRequest.payload.contains("to_account") - (consentRequestJson, isVRPConsentRequest) <- - if(isVrpConsent) { - val failMsg = s"$InvalidJsonFormat The vrp consent request json body should be the $PostVRPConsentRequestJsonV510 " - NewStyle.function.tryons(failMsg, 400, callContext) { - json.parse(createdConsentRequest.payload).extract[code.api.v5_1_0.PostVRPConsentRequestJsonInternalV510] - }.map(postVRPConsentRequest => (postVRPConsentRequest.toPostConsentRequestJsonV500, true)) - } else{ - val failMsg = s"$InvalidJsonFormat The consent request Json body should be the $PostConsentRequestJsonV500 " - NewStyle.function.tryons(failMsg, 400, callContext) { - json.parse(createdConsentRequest.payload).extract[PostConsentRequestJsonV500] - }.map(postVRPConsentRequest => (postVRPConsentRequest, false)) - } - - //Here are all the VRP consent request - (bankId, accountId, viewId, counterpartyId) <- if (isVRPConsentRequest) { - val postConsentRequestJsonV510 = json.parse(createdConsentRequest.payload).extract[code.api.v5_1_0.PostVRPConsentRequestJsonV510] - - val vrpViewId = s"_vrp-${UUID.randomUUID.toString}".dropRight(5)// to make sure the length of the viewId is 36. - val targetPermissions = List(//may need getTransactionRequest . so far only these payments. - CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY, - CAN_GET_COUNTERPARTY, - CAN_SEE_TRANSACTION_REQUESTS, - ) - - val targetCreateCustomViewJson = CreateCustomViewJson( - name = vrpViewId, - description = vrpViewId, - metadata_view = vrpViewId, - is_public = false, - which_alias_to_use = vrpViewId, - hide_metadata_if_alias_used = true, - allowed_permissions = targetPermissions - ) - - val fromBankAccountRoutings = BankAccountRoutings( - bank = BankRoutingJson(postConsentRequestJsonV510.from_account.bank_routing.scheme, postConsentRequestJsonV510.from_account.bank_routing.address), - account = BranchRoutingJsonV141(postConsentRequestJsonV510.from_account.account_routing.scheme, postConsentRequestJsonV510.from_account.account_routing.address), - branch = AccountRoutingJsonV121(postConsentRequestJsonV510.from_account.branch_routing.scheme, postConsentRequestJsonV510.from_account.branch_routing.address) - ) - - for { - //1st: get the fromAccount by routings: - (fromAccount, callContext) <- NewStyle.function.getBankAccountByRoutings(fromBankAccountRoutings, callContext) - fromBankIdAccountId = BankIdAccountId(fromAccount.bankId, fromAccount.accountId) - - //2rd: create the Custom View for the fromAccount. - //we do not need sourceViewId so far, we need to get all the view access for the login user, and - permission <- NewStyle.function.permission(fromAccount.bankId, fromAccount.accountId, user, callContext) - permissionsFromSource = permission.views.map(_.allowed_actions).flatten.toSet - permissionsFromTarget = targetCreateCustomViewJson.allowed_permissions - - //eg: permissionsFromTarget=List(1,2), permissionsFromSource = List(1,3,4) => userMissingPermissions = List(2) - //Here would find the missing permissions and show them in the error messages - userMissingPermissions = permissionsFromTarget.toSet diff permissionsFromSource - - failMsg = s"${ErrorMessages.UserDoesNotHavePermission} ${userMissingPermissions.toString}" - _ <- Helper.booleanToFuture(failMsg, cc = callContext) { - userMissingPermissions.isEmpty - } - (vrpView, callContext) <- ViewNewStyle.createCustomView(fromBankIdAccountId, targetCreateCustomViewJson.toCreateViewJson, callContext) - - _ <-ViewNewStyle.grantAccessToCustomView(vrpView, user, callContext) - - //3rd: Create a new counterparty on that view (_VRP-9d429899-24f5-42c8-8565-943ffa6a7945) - postJson = PostCounterpartyJson400( - name = postConsentRequestJsonV510.to_account.counterparty_name, - description = postConsentRequestJsonV510.to_account.counterparty_name, - currency = postConsentRequestJsonV510.to_account.limit.currency, - other_account_routing_scheme = StringHelpers.snakify(postConsentRequestJsonV510.to_account.account_routing.scheme).toUpperCase, - other_account_routing_address = postConsentRequestJsonV510.to_account.account_routing.address, - other_account_secondary_routing_scheme = "", - other_account_secondary_routing_address = "", - other_bank_routing_scheme = StringHelpers.snakify(postConsentRequestJsonV510.to_account.bank_routing.scheme).toUpperCase, - other_bank_routing_address = postConsentRequestJsonV510.to_account.bank_routing.address, - other_branch_routing_scheme = StringHelpers.snakify(postConsentRequestJsonV510.to_account.branch_routing.scheme).toUpperCase, - other_branch_routing_address = postConsentRequestJsonV510.to_account.branch_routing.address, - is_beneficiary = true, - bespoke = Nil - ) - _ <- Helper.booleanToFuture(s"$InvalidValueLength. The maximum length of `description` field is ${MappedCounterparty.mDescription.maxLen}", cc = callContext) { - postJson.description.length <= 36 - } - - - (counterparty, callContext) <- Connector.connector.vend.checkCounterpartyExists(postJson.name, fromBankIdAccountId.bankId.value, fromBankIdAccountId.accountId.value, vrpView.viewId.value, callContext) - - _ <- Helper.booleanToFuture(CounterpartyAlreadyExists.replace("value for BANK_ID or ACCOUNT_ID or VIEW_ID or NAME.", - s"COUNTERPARTY_NAME(${postJson.name}) for the BANK_ID(${fromBankIdAccountId.bankId.value}) and ACCOUNT_ID(${fromBankIdAccountId.accountId.value}) and VIEW_ID($vrpViewId)"), cc = callContext) { - counterparty.isEmpty - } - - _ <- Helper.booleanToFuture(s"$InvalidISOCurrencyCode Current input is: '${postJson.currency}'", cc = callContext) { - isValidCurrencyISOCode(postJson.currency) - } - - (counterparty, callContext) <- NewStyle.function.createCounterparty( - name = postJson.name, - description = postJson.description, - currency = postJson.currency, - createdByUserId = user.userId, - thisBankId = fromBankIdAccountId.bankId.value, - thisAccountId = fromBankIdAccountId.accountId.value, - thisViewId = vrpViewId, - otherAccountRoutingScheme = postJson.other_account_routing_scheme, - otherAccountRoutingAddress = postJson.other_account_routing_address, - otherAccountSecondaryRoutingScheme = postJson.other_account_secondary_routing_scheme, - otherAccountSecondaryRoutingAddress = postJson.other_account_secondary_routing_address, - otherBankRoutingScheme = postJson.other_bank_routing_scheme, - otherBankRoutingAddress = postJson.other_bank_routing_address, - otherBranchRoutingScheme = postJson.other_branch_routing_scheme, - otherBranchRoutingAddress = postJson.other_branch_routing_address, - isBeneficiary = postJson.is_beneficiary, - bespoke = postJson.bespoke.map(bespoke => CounterpartyBespoke(bespoke.key, bespoke.value)), - callContext - ) - - postCounterpartyLimitV510 = PostCounterpartyLimitV510( - currency = postConsentRequestJsonV510.to_account.limit.currency, - max_single_amount = postConsentRequestJsonV510.to_account.limit.max_single_amount, - max_monthly_amount = postConsentRequestJsonV510.to_account.limit.max_monthly_amount, - max_number_of_monthly_transactions = postConsentRequestJsonV510.to_account.limit.max_number_of_monthly_transactions, - max_yearly_amount = postConsentRequestJsonV510.to_account.limit.max_yearly_amount, - max_number_of_yearly_transactions = postConsentRequestJsonV510.to_account.limit.max_number_of_yearly_transactions, - max_total_amount = postConsentRequestJsonV510.to_account.limit.max_total_amount, - max_number_of_transactions = postConsentRequestJsonV510.to_account.limit.max_number_of_transactions - ) - - //4th: create the counterparty limit - (counterpartyLimitBox, callContext) <- Connector.connector.vend.getCounterpartyLimit( - fromBankIdAccountId.bankId.value, - fromBankIdAccountId.accountId.value, - vrpViewId, - counterparty.counterpartyId, - cc.callContext - ) - failMsg = s"$CounterpartyLimitAlreadyExists Current BANK_ID(${fromBankIdAccountId.bankId.value}), " + - s"ACCOUNT_ID(${fromBankIdAccountId.accountId.value}), VIEW_ID($vrpViewId),COUNTERPARTY_ID(${counterparty.counterpartyId})" - _ <- Helper.booleanToFuture(failMsg, cc = callContext) { - counterpartyLimitBox.isEmpty - } - (counterpartyLimit, callContext) <- NewStyle.function.createOrUpdateCounterpartyLimit( - bankId = counterparty.thisBankId, - accountId = counterparty.thisAccountId, - viewId = counterparty.thisViewId, - counterpartyId = counterparty.counterpartyId, - postCounterpartyLimitV510.currency, - BigDecimal(postCounterpartyLimitV510.max_single_amount), - BigDecimal(postCounterpartyLimitV510.max_monthly_amount), - postCounterpartyLimitV510.max_number_of_monthly_transactions, - BigDecimal(postCounterpartyLimitV510.max_yearly_amount), - postCounterpartyLimitV510.max_number_of_yearly_transactions, - BigDecimal(postCounterpartyLimitV510.max_total_amount), - postCounterpartyLimitV510.max_number_of_transactions, - cc.callContext - ) - - } yield { - (fromAccount.bankId, fromAccount.accountId, vrpView.viewId, CounterpartyId(counterparty.counterpartyId)) - } - }else{ - Future.successful(BankId(""), AccountId(""), ViewId(""),CounterpartyId("")) - } - - maxTimeToLive = APIUtil.getPropsAsIntValue(nameOfProperty="consents.max_time_to_live", defaultValue=3600) - _ <- Helper.booleanToFuture(s"$ConsentMaxTTL ($maxTimeToLive)", cc=callContext){ - consentRequestJson.time_to_live match { - case Some(ttl) => ttl <= maxTimeToLive - case _ => true - } - } - requestedEntitlements = consentRequestJson.entitlements.getOrElse(Nil) - myEntitlements <- Entitlement.entitlement.vend.getEntitlementsByUserIdFuture(user.userId) - _ <- Helper.booleanToFuture(RolesForbiddenInConsent, cc=callContext){ - requestedEntitlements.map(_.role_name) - .intersect( - List( - canCreateEntitlementAtOneBank.toString(), - canCreateEntitlementAtAnyBank.toString()) - ).length == 0 - } - _ <- Helper.booleanToFuture(RolesAllowedInConsent, cc=callContext){ - requestedEntitlements.forall( - re => myEntitlements.getOrElse(Nil).exists( - e => e.roleName == re.role_name && e.bankId == re.bank_id - ) - ) - } - postConsentViewJsons <- if(isVrpConsent) { - Future.successful(List(PostConsentViewJsonV310( - bankId.value, - accountId.value, - viewId.value - ))) - }else{ - Future.sequence( - consentRequestJson.account_access.map( - access => - NewStyle.function.getBankAccountByRouting(consentRequestJson.bank_id.map(BankId(_)),access.account_routing.scheme, access.account_routing.address, cc.callContext) - .map(result =>PostConsentViewJsonV310( - result._1.bankId.value, - result._1.accountId.value, - access.view_id - )) - ) - ) - } - - (_, assignedViews) <- Future(Views.views.vend.privateViewsUserCanAccess(user)) - _ <- Helper.booleanToFuture(ViewsAllowedInConsent, cc=callContext){ - postConsentViewJsons.forall( - rv => assignedViews.exists{ - e => - e.view_id == rv.view_id && - e.bank_id == rv.bank_id && - e.account_id == rv.account_id - } - ) - } - // Use consumer specified at the payload of consent request in preference to the field ConsumerId of consent request - // i.e. ConsentRequest.Payload.consumer_id in preference to ConsentRequest.ConsumerId - calculatedConsumerId = consentRequestJson.consumer_id.orElse(Some(createdConsentRequest.consumerId)) - (consumerId, applicationText) <- calculatedConsumerId match { - case Some(id) => NewStyle.function.checkConsumerByConsumerId(id, callContext) map { - c => (Some(c.consumerId.get), c.description) - } - case None => Future(None, "Any application") - } - - challengeAnswer = Props.mode match { - case Props.RunModes.Test => Consent.challengeAnswerAtTestEnvironment - case _ => SecureRandomUtil.numeric() - } - consumer = Consumers.consumers.vend.getConsumerByConsumerId(calculatedConsumerId.getOrElse("None")) - createdConsent <- Future( - Consents.consentProvider.vend.createObpConsent( - user, - challengeAnswer, - Some(consentRequestId), - consumer - ) - ) map { - i => connectorEmptyResponse(i, callContext) - } - - postConsentBodyCommonJson = PostConsentBodyCommonJson( - everything = consentRequestJson.everything, - bank_id = consentRequestJson.bank_id, - views = postConsentViewJsons, - entitlements = consentRequestJson.entitlements.getOrElse(Nil), - consumer_id = consentRequestJson.consumer_id, - consent_request_id = Some(consentRequestId), - valid_from = consentRequestJson.valid_from, - time_to_live = consentRequestJson.time_to_live, - ) - - consentJWT = Consent.createConsentJWT( - user, - postConsentBodyCommonJson, - createdConsent.secret, - createdConsent.consentId, - consumerId, - postConsentBodyCommonJson.valid_from, - postConsentBodyCommonJson.time_to_live.getOrElse(3600), - Some(HelperInfoJson(List(counterpartyId.value))) - ) - _ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map { - i => connectorEmptyResponse(i, callContext) - } - validUntil = Helper.calculateValidTo(postConsentBodyCommonJson.valid_from, postConsentBodyCommonJson.time_to_live.getOrElse(3600)) - _ <- Future(Consents.consentProvider.vend.setValidUntil(createdConsent.consentId, validUntil)) map { - i => connectorEmptyResponse(i, callContext) - } - //we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow. - //this is from callContext - grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown") - //this is from json body - granteeConsumerId = postConsentBodyCommonJson.consumer_id.getOrElse("Unknown") - - shouldSkipConsentScaForConsumerIdPair = APIUtil.skipConsentScaForConsumerIdPairs.contains( - APIUtil.ConsumerIdPair( - grantorConsumerId, - granteeConsumerId - )) - mappedConsent <- if (shouldSkipConsentScaForConsumerIdPair) { - Future{ - MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head - } - } else { - val challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText" - scaMethod match { - case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email - sendEmailNotification(callContext, consentRequestJson, challengeText) - case v if v == StrongCustomerAuthentication.SMS.toString => - sendSmsNotification(callContext, consentRequestJson, challengeText) - case v if v == StrongCustomerAuthentication.IMPLICIT.toString => - for { - (consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext) - status <- consentImplicitSCA.scaMethod match { - case v if v == StrongCustomerAuthentication.EMAIL => // Send the email - sendEmailNotification(callContext, consentRequestJson.copy(email = Some(consentImplicitSCA.recipient)), challengeText) - case v if v == StrongCustomerAuthentication.SMS => - sendSmsNotification(callContext, consentRequestJson.copy(phone_number = Some(consentImplicitSCA.recipient)), challengeText) - case _ => Future { - "Success" - } - }} yield { - status - } - case _ => Future { - "Success" - } - } - Future{createdConsent} - } - } yield { - (ConsentJsonV500( - mappedConsent.consentId, - consentJWT, - mappedConsent.status, - Some(mappedConsent.consentRequestId), - if (isVRPConsentRequest) Some( - ConsentAccountAccessJson( - bankId.value, - accountId.value, - viewId.value, - Some(HelperInfoJson(List(counterpartyId.value)))) - ) - else None - ), HttpCode.`201`(callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - headAtms, - implementedInApiVersion, - nameOf(headAtms), - "HEAD", - "/banks/BANK_ID/atms", - "Head Bank ATMS", - s"""Head Bank ATMS.""", - EmptyBody, - atmsJsonV400, - List( - $BankNotFound, - UnknownError - ), - List(apiTagATM) - ) - lazy val headAtms : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "atms" :: Nil JsonHead _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, callContext) <- getAtmsIsPublic match { - case false => authenticatedAccess(cc) - case true => anonymousAccess(cc) - } - } yield { - ("", HttpCode.`200`(callContext)) - } - } - } - - - - staticResourceDocs += ResourceDoc( - createCustomer, - implementedInApiVersion, - nameOf(createCustomer), - "POST", - "/banks/BANK_ID/customers", - "Create Customer", - s""" - |The Customer resource stores the customer number (which is set by the backend), legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc. - |Dates need to be in the format 2013-01-21T23:08:00Z - | - |If kyc_status is not provided, it defaults to false. - | - |Note: If you need to set a specific customer number, use the Update Customer Number endpoint after this call. - | - |${userAuthenticationMessage(true)} - |""", - postCustomerJsonV500, - customerJsonV310, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - InvalidJsonFormat, - CustomerNumberAlreadyExists, - UserNotFoundById, - CustomerAlreadyExistsForUser, - CreateConsumerError, - UnknownError - ), - List(apiTagCustomer, apiTagPerson), - Some(List(canCreateCustomer,canCreateCustomerAtAnyBank)) - ) - lazy val createCustomer : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customers" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerJsonV500 ", 400, cc.callContext) { - json.extract[PostCustomerJsonV500] - } - _ <- Helper.booleanToFuture(failMsg = InvalidJsonContent + s" The field dependants(${postedData.dependants.getOrElse(0)}) not equal the length(${postedData.dob_of_dependants.getOrElse(Nil).length }) of dob_of_dependants array", 400, cc.callContext) { - postedData.dependants.getOrElse(0) == postedData.dob_of_dependants.getOrElse(Nil).length - } - customerNumber = postedData.customer_number.getOrElse(Random.nextInt(Integer.MAX_VALUE).toString) - - _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat customer_number can not contain `::::` characters", cc=cc.callContext) { - !`checkIfContains::::` (customerNumber) - } - (_, callContext) <- NewStyle.function.checkCustomerNumberAvailable(bankId, customerNumber, cc.callContext) - (customer, callContext) <- NewStyle.function.createCustomerC2( - bankId, - postedData.legal_name, - customerNumber, - postedData.mobile_phone_number, - postedData.email.getOrElse(""), - CustomerFaceImage( - postedData.face_image.map(_.date).getOrElse(null), - postedData.face_image.map(_.url).getOrElse("") - ), - postedData.date_of_birth.getOrElse(null), - postedData.relationship_status.getOrElse(""), - postedData.dependants.getOrElse(0), - postedData.dob_of_dependants.getOrElse(Nil), - postedData.highest_education_attained.getOrElse(""), - postedData.employment_status.getOrElse(""), - postedData.kyc_status.getOrElse(false), - postedData.last_ok_date.getOrElse(null), - postedData.credit_rating.map(i => CreditRating(i.rating, i.source)), - postedData.credit_limit.map(i => CreditLimit(i.currency, i.amount)), - postedData.title.getOrElse(""), - postedData.branch_id.getOrElse(""), - postedData.name_suffix.getOrElse(""), - "", - "", - callContext, - ) - } yield { - (JSONFactory310.createCustomerJson(customer), HttpCode.`201`(callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - getCustomerOverview, - implementedInApiVersion, - nameOf(getCustomerOverview), - "POST", - "/banks/BANK_ID/customers/customer-number-query/overview", - "Get Customer Overview", - s"""Gets the Customer Overview specified by customer_number and bank_code. - | - | - |${userAuthenticationMessage(true)} - | - |""", - postCustomerOverviewJsonV500, - customerOverviewJsonV500, - List( - AuthenticatedUserIsRequired, - UserCustomerLinksNotFoundForUser, - UnknownError - ), - List(apiTagCustomer, apiTagKyc), - Some(List(canGetCustomerOverview)) - ) - - lazy val getCustomerOverview : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customers" :: "customer-number-query" :: "overview" :: Nil JsonPost json -> req => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 ", 400, cc.callContext) { - json.extract[PostCustomerOverviewJsonV500] - } - (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bankId, cc.callContext) - (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( - bankId, - CustomerId(customer.customerId), - callContext: Option[CallContext]) - accountIds <- AccountAttributeX.accountAttributeProvider.vend - .getAccountIdsByParams(bankId, List("customer_number" -> List(postedData.customer_number)).toMap) - (accounts: List[BankAccount], callContext) <- NewStyle.function.getBankAccounts(accountIds.toList.flatten.map(i => BankIdAccountId(bankId, AccountId(i))), callContext) - (accountAttributes, callContext) <- NewStyle.function.getAccountAttributesForAccounts( - bankId, - accounts, - callContext: Option[CallContext]) - } yield { - (JSONFactory500.createCustomerWithAttributesJson(customer, customerAttributes, accountAttributes), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getCustomerOverviewFlat, - implementedInApiVersion, - nameOf(getCustomerOverviewFlat), - "POST", - "/banks/BANK_ID/customers/customer-number-query/overview-flat", - "Get Customer Overview Flat", - s"""Gets the Customer Overview Flat specified by customer_number and bank_code. - | - | - |${userAuthenticationMessage(true)} - | - |""", - postCustomerOverviewJsonV500, - customerOverviewFlatJsonV500, - List( - AuthenticatedUserIsRequired, - UserCustomerLinksNotFoundForUser, - UnknownError - ), - List(apiTagCustomer, apiTagKyc), - Some(List(canGetCustomerOverviewFlat)) - ) - - lazy val getCustomerOverviewFlat : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customers" :: "customer-number-query" :: "overview-flat" :: Nil JsonPost json -> req => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 ", 400, cc.callContext) { - json.extract[PostCustomerOverviewJsonV500] - } - (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bankId, cc.callContext) - (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( - bankId, - CustomerId(customer.customerId), - callContext: Option[CallContext]) - accountIds <- AccountAttributeX.accountAttributeProvider.vend - .getAccountIdsByParams(bankId, List("customer_number" -> List(postedData.customer_number)).toMap) - (accounts: List[BankAccount], callContext) <- NewStyle.function.getBankAccounts(accountIds.toList.flatten.map(i => BankIdAccountId(bankId, AccountId(i))), callContext) - (accountAttributes, callContext) <- NewStyle.function.getAccountAttributesForAccounts( - bankId, - accounts, - callContext: Option[CallContext]) - } yield { - (JSONFactory500.createCustomerOverviewFlatJson(customer, customerAttributes, accountAttributes), HttpCode.`200`(callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - getMyCustomersAtAnyBank, - implementedInApiVersion, - nameOf(getMyCustomersAtAnyBank), - "GET", - "/my/customers", - "Get My Customers", - """Gets all Customers that are linked to me. - | - |Authentication via OAuth is required.""", - EmptyBody, - customerJsonV210, - List( - $AuthenticatedUserIsRequired, - UserCustomerLinksNotFoundForUser, - UnknownError - ), - List(apiTagCustomer, apiTagUser)) - - lazy val getMyCustomersAtAnyBank : OBPEndpoint = { - case "my" :: "customers" :: Nil JsonGet _ => { - cc => { - implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), callContext) <- SS.user - (customers, callContext) <- Connector.connector.vend.getCustomersByUserId(u.userId, callContext) map { - connectorEmptyResponse(_, callContext) - } - } yield { - (JSONFactory210.createCustomersJson(customers), HttpCode.`200`(callContext)) - } - } - } - } - - staticResourceDocs += ResourceDoc( - getMyCustomersAtBank, - implementedInApiVersion, - nameOf(getMyCustomersAtBank), - "GET", - "/banks/BANK_ID/my/customers", - "Get My Customers at Bank", - s"""Returns a list of Customers at the Bank that are linked to the currently authenticated User. - | - | - |${userAuthenticationMessage(true)}""".stripMargin, - EmptyBody, - customerJSONs, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UserCustomerLinksNotFoundForUser, - UnknownError - ), - List(apiTagCustomer) - ) - - lazy val getMyCustomersAtBank : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "my" :: "customers" :: Nil JsonGet _ => { - cc => { - implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), callContext) <- SS.user - (_, callContext) <- NewStyle.function.getBank(bankId, callContext) - (customers, callContext) <- Connector.connector.vend.getCustomersByUserId(u.userId, callContext) map { - connectorEmptyResponse(_, callContext) - } - } yield { - // Filter so we only see the ones for the bank in question - val bankCustomers = customers.filter(_.bankId==bankId.value) - val json = JSONFactory210.createCustomersJson(bankCustomers) - (json, HttpCode.`200`(callContext)) - } - } - } - } - - - staticResourceDocs += ResourceDoc( - getCustomersAtOneBank, - implementedInApiVersion, - nameOf(getCustomersAtOneBank), - "GET", - "/banks/BANK_ID/customers", - "Get Customers at Bank", - s"""Get Customers at Bank. - | - | - |${userAuthenticationMessage(true)} - | - |""", - EmptyBody, - customersJsonV300, - List( - AuthenticatedUserIsRequired, - UserCustomerLinksNotFoundForUser, - UnknownError - ), - List(apiTagCustomer, apiTagUser), - Some(List(canGetCustomersAtOneBank)) - ) - - lazy val getCustomersAtOneBank : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customers" :: Nil JsonGet _ => { - cc => { - implicit val ec = EndpointContext(Some(cc)) - for { - (requestParams, callContext) <- extractQueryParams(cc.url, List("limit","offset","sort_direction"), cc.callContext) - customers <- NewStyle.function.getCustomers(bankId, callContext, requestParams) - } yield { - (JSONFactory300.createCustomersJson(customers.sortBy(_.bankId)), HttpCode.`200`(callContext)) - } - } - } - } - - staticResourceDocs += ResourceDoc( - getCustomersMinimalAtOneBank, - implementedInApiVersion, - nameOf(getCustomersMinimalAtOneBank), - "GET", - "/banks/BANK_ID/customers-minimal", - "Get Customers Minimal at Bank", - s"""Get Customers Minimal at Bank. - | - | - | - |""", - EmptyBody, - customersMinimalJsonV300, - List( - UserCustomerLinksNotFoundForUser, - UnknownError - ), - List(apiTagCustomer, apiTagUser), - Some(List(canGetCustomersMinimalAtOneBank)) - ) - lazy val getCustomersMinimalAtOneBank : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customers-minimal" :: Nil JsonGet _ => { - cc => { - implicit val ec = EndpointContext(Some(cc)) - for { - (requestParams, callContext) <- extractQueryParams(cc.url, List("limit","offset","sort_direction"), cc.callContext) - customers <- NewStyle.function.getCustomers(bankId, callContext, requestParams) - } yield { - (createCustomersMinimalJson(customers.sortBy(_.bankId)), HttpCode.`200`(callContext)) - } - } - } - } - - - staticResourceDocs += ResourceDoc( - createProduct, - implementedInApiVersion, - nameOf(createProduct), - "PUT", - "/banks/BANK_ID/products/PRODUCT_CODE", - "Create Product", - s"""Create or Update Product for the Bank. - | - |The combination of bank_id and product_code is unique. If a Product already exists for the bank_id and product_code, it will be updated. - | - |Typical Super Family values / Asset classes are: - | - |Debt - |Equity - |FX - |Commodity - |Derivative - | - |$productHiearchyAndCollectionNote - | - | - |${userAuthenticationMessage(true) } - | - | - |""", - putProductJsonV500, - productJsonV400.copy(attributes = None, fees = None), - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UserHasMissingRoles, - UnknownError - ), - List(apiTagProduct), - Some(List(canCreateProduct, canCreateProductAtAnyBank)) - ) - lazy val createProduct: OBPEndpoint = { - case "banks" :: BankId(bankId) :: "products" :: ProductCode(productCode) :: Nil JsonPut json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), callContext) <- SS.user - _ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = createProductEntitlementsRequiredText)(bankId.value, u.userId, createProductEntitlements, callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $PutProductJsonV500 " - product <- NewStyle.function.tryons(failMsg, 400, callContext) { - json.extract[PutProductJsonV500] - } - (parentProduct, callContext) <- product.parent_product_code.trim.nonEmpty match { - case false => - Future((Empty, callContext)) - case true => - NewStyle.function.getProduct(bankId, ProductCode(product.parent_product_code), callContext).map(product => (Full(product._1),product._2)) - } - (success, callContext) <- NewStyle.function.createOrUpdateProduct( - bankId = bankId.value, - code = productCode.value, - parentProductCode = parentProduct.map(_.code.value).toOption, - name = product.name, - category = null, - family = null, - superFamily = null, - moreInfoUrl = product.more_info_url.getOrElse(""), - termsAndConditionsUrl = product.terms_and_conditions_url.getOrElse(""), - details = null, - description = product.description.getOrElse(""), - metaLicenceId = product.meta.map(_.license.id).getOrElse(""), - metaLicenceName = product.meta.map(_.license.name).getOrElse(""), - callContext - ) - } yield { - (JSONFactory400.createProductJson(success), HttpCode.`201`(callContext)) - } - } - } - - - - staticResourceDocs += ResourceDoc( - addCardForBank, - implementedInApiVersion, - nameOf(addCardForBank), - "POST", - "/management/banks/BANK_ID/cards", - "Create Card", - s"""Create Card at bank specified by BANK_ID . - | - |${userAuthenticationMessage(true)} - |""", - createPhysicalCardJsonV500, - physicalCardJsonV500, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UserHasMissingRoles, - AllowedValuesAre, - UnknownError - ), - List(apiTagCard), - Some(List(canCreateCardsForBank))) - lazy val addCardForBank: OBPEndpoint = { - case "management" :: "banks" :: BankId(bankId) :: "cards" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), _,callContext) <- SS.userBank - - failMsg = s"$InvalidJsonFormat The Json body should be the $CreatePhysicalCardJsonV500 " - postJson <- NewStyle.function.tryons(failMsg, 400, callContext) {json.extract[CreatePhysicalCardJsonV500]} - - _ <- postJson.allows match { - case List() => Future {true} - case _ => Helper.booleanToFuture(AllowedValuesAre + CardAction.availableValues.mkString(", "), cc=callContext)(postJson.allows.forall(a => CardAction.availableValues.contains(a))) - } - - failMsg = AllowedValuesAre + CardReplacementReason.availableValues.mkString(", ") - cardReplacementReason <- NewStyle.function.tryons(failMsg, 400, callContext) { - postJson.replacement match { - case Some(value) => CardReplacementReason.valueOf(value.reason_requested) - case None => CardReplacementReason.valueOf(CardReplacementReason.FIRST.toString) - } - } - - _<-Helper.booleanToFuture(s"${maximumLimitExceeded.replace("10000", "10")} Current issue_number is ${postJson.issue_number}", cc=callContext)(postJson.issue_number.length<= 10) - - (_, callContext)<- NewStyle.function.getBankAccount(bankId, AccountId(postJson.account_id), callContext) - - (_, callContext)<- NewStyle.function.getCustomerByCustomerId(postJson.customer_id, callContext) - - replacement = postJson.replacement match { - case Some(replacement) => - Some(CardReplacementInfo(requestedDate = replacement.requested_date, cardReplacementReason)) - case None => None - } - collected = postJson.collected match { - case Some(collected) => Some(CardCollectionInfo(collected)) - case None => None - } - posted = postJson.posted match { - case Some(posted) => Option(CardPostedInfo(posted)) - case None => None - } - - cvv = ThreadLocalRandom.current().nextLong(100, 999) - - (card, callContext) <- NewStyle.function.createPhysicalCard( - bankCardNumber=postJson.card_number, - nameOnCard=postJson.name_on_card, - cardType = postJson.card_type, - issueNumber=postJson.issue_number, - serialNumber=postJson.serial_number, - validFrom=postJson.valid_from_date, - expires=postJson.expires_date, - enabled=postJson.enabled, - cancelled=false, - onHotList=false, - technology=postJson.technology, - networks= postJson.networks, - allows= postJson.allows, - accountId= postJson.account_id, - bankId=bankId.value, - replacement = replacement, - pinResets= postJson.pin_reset.map(e => PinResetInfo(e.requested_date, PinResetReason.valueOf(e.reason_requested.toUpperCase))), - collected = collected, - posted = posted, - customerId = postJson.customer_id, - cvv = cvv.toString, - brand = postJson.brand, - callContext - ) - } yield { - //NOTE: OBP do not store the 3 digits cvv, only the hash, so we copy it here. - (createPhysicalCardJson(card, u).copy(cvv=cvv.toString), HttpCode.`201`(callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - getViewsForBankAccount, - implementedInApiVersion, - nameOf(getViewsForBankAccount), - "GET", - "/banks/BANK_ID/accounts/ACCOUNT_ID/views", - "Get Views for Account", - s"""#Views - | - | - |Views in Open Bank Project provide a mechanism for fine grained access control and delegation to Accounts and Transactions. Account holders use the 'owner' view by default. Delegated access is made through other views for example 'accountants', 'share-holders' or 'tagging-application'. Views can be created via the API and each view has a list of entitlements. - | - |Views on accounts and transactions filter the underlying data to redact certain fields for certain users. For instance the balance on an account may be hidden from the public. The way to know what is possible on a view is determined in the following JSON. - | - |**Data:** When a view moderates a set of data, some fields my contain the value `null` rather than the original value. This indicates either that the user is not allowed to see the original data or the field is empty. - | - |There is currently one exception to this rule; the 'holder' field in the JSON contains always a value which is either an alias or the real name - indicated by the 'is_alias' field. - | - |**Action:** When a user performs an action like trying to post a comment (with POST API call), if he is not allowed, the body response will contain an error message. - | - |**Metadata:** - |Transaction metadata (like images, tags, comments, etc.) will appears *ONLY* on the view where they have been created e.g. comments posted to the public view only appear on the public view. - | - |The other account metadata fields (like image_URL, more_info, etc.) are unique through all the views. Example, if a user edits the 'more_info' field in the 'team' view, then the view 'authorities' will show the new value (if it is allowed to do it). - | - |# All - |*Optional* - | - |Returns the list of the views created for account ACCOUNT_ID at BANK_ID. - | - |${userAuthenticationMessage(true)} and the user needs to have access to the owner view.""", - EmptyBody, - viewsJsonV500, - List( - $AuthenticatedUserIsRequired, - $BankAccountNotFound, - UnknownError - ), - List(apiTagView, apiTagAccount)) - - lazy val getViewsForBankAccount : OBPEndpoint = { - //get the available views on an bank account - case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: Nil JsonGet req => { - cc => implicit val ec = EndpointContext(Some(cc)) - val res = - for { - (Full(u), callContext) <- SS.user - permission <- NewStyle.function.permission(bankId, accountId, u, callContext) - anyViewContainsCanSeeAvailableViewsForBankAccountPermission = permission.views.map(_.allowed_actions.exists(_ == CAN_SEE_AVAILABLE_VIEWS_FOR_BANK_ACCOUNT)).find(_.==(true)).getOrElse(false) - _ <- Helper.booleanToFuture( - s"${ErrorMessages.ViewDoesNotPermitAccess} You need the `${(CAN_SEE_AVAILABLE_VIEWS_FOR_BANK_ACCOUNT)}` permission on any your views", - cc = callContext - ) { - anyViewContainsCanSeeAvailableViewsForBankAccountPermission - } - } yield { - for { - views <- Full(Views.views.vend.availableViewsForAccount(BankIdAccountId(bankId, accountId))) - } yield { - (createViewsJsonV500(views), HttpCode.`200`(cc.callContext)) - } - } - res map { fullBoxOrException(_) } map { unboxFull(_) } - } - } - - staticResourceDocs += ResourceDoc( - deleteSystemView, - implementedInApiVersion, - "deleteSystemView", - "DELETE", - "/system-views/VIEW_ID", - "Delete System View", - "Deletes the system view specified by VIEW_ID", - EmptyBody, - EmptyBody, - List( - AuthenticatedUserIsRequired, - BankAccountNotFound, - UnknownError, - "user does not have owner access" - ), - List(apiTagSystemView), - Some(List(canDeleteSystemView)) - ) - - lazy val deleteSystemView: OBPEndpoint = { - case "system-views" :: viewId :: Nil JsonDelete req => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - _ <- ViewNewStyle.systemView(ViewId(viewId), cc.callContext) - view <- ViewNewStyle.deleteSystemView(ViewId(viewId), cc.callContext) - } yield { - (Full(view), HttpCode.`200`(cc.callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getMetricsAtBank, - implementedInApiVersion, - nameOf(getMetricsAtBank), - "GET", - "/management/metrics/banks/BANK_ID", - "Get Metrics at Bank", - s"""Get the all metrics at the Bank specified by BANK_ID - | - |require CanReadMetrics role - | - |Filters Part 1.*filtering* (no wilde cards etc.) parameters to GET /management/metrics - | - |Should be able to filter on the following metrics fields - | - |eg: /management/metrics?from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString&limit=50&offset=2 - | - |1 from_date (defaults to one week before current date): eg:from_date=$DateWithMsExampleString - | - |2 to_date (defaults to current date) eg:to_date=$DateWithMsExampleString - | - |3 limit (for pagination: defaults to 50) eg:limit=200 - | - |4 offset (for pagination: zero index, defaults to 0) eg: offset=10 - | - |5 sort_by (defaults to date field) eg: sort_by=date - | possible values: - | "url", - | "date", - | "username" (or "user_name" for backward compatibility), - | "app_name", - | "developer_email", - | "implemented_by_partial_function", - | "implemented_in_version", - | "consumer_id", - | "verb" - | - |6 direction (defaults to date desc) eg: direction=desc - | - |eg: /management/metrics?from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString&limit=10000&offset=0&anon=false&app_name=TeatApp&implemented_in_version=v2.1.0&verb=POST&user_id=c7b6cb47-cb96-4441-8801-35b57456753a&username=susan.uk.29@example.com&consumer_id=78 - | - |Other filters: - | - |7 consumer_id (if null ignore) - | - |8 user_id (if null ignore) - | - |9 anon (if null ignore) only support two value : true (return where user_id is null.) or false (return where user_id is not null.) - | - |10 url (if null ignore), note: can not contain '&'. - | - |11 app_name (if null ignore) - | - |12 implemented_by_partial_function (if null ignore), - | - |13 implemented_in_version (if null ignore) - | - |14 verb (if null ignore) - | - |15 correlation_id (if null ignore) - | - |16 duration (if null ignore) non digit chars will be silently omitted - | - """.stripMargin, - EmptyBody, - metricsJson, - List( - $AuthenticatedUserIsRequired, - UserHasMissingRoles, - UnknownError - ), - List(apiTagMetric, apiTagApi), - Some(List(canGetMetricsAtOneBank))) - - lazy val getMetricsAtBank : OBPEndpoint = { - case "management" :: "metrics" :: "banks" :: bankId :: Nil JsonGet _ => { - cc => { - implicit val ec = EndpointContext(Some(cc)) - for { - httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) - (obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext) - metrics <- Future(APIMetrics.apiMetrics.vend.getAllMetrics(obpQueryParams ::: List(OBPBankId(bankId)))) - } yield { - (JSONFactory210.createMetricsJson(metrics), HttpCode.`200`(callContext)) - } - } - } - } - - staticResourceDocs += ResourceDoc( - getSystemView, - implementedInApiVersion, - "getSystemView", - "GET", - "/system-views/VIEW_ID", - "Get System View", - s"""Get System View - | - |${userAuthenticationMessage(true)} - | - """.stripMargin, - EmptyBody, - viewJsonV500, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UnknownError - ), - List(apiTagSystemView), - Some(List(canGetSystemView)) - ) - - lazy val getSystemView: OBPEndpoint = { - case "system-views" :: viewId :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - view <- ViewNewStyle.systemView(ViewId(viewId), cc.callContext) - } yield { - (createViewJsonV500(view), HttpCode.`200`(cc.callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getSystemViewsIds, - implementedInApiVersion, - nameOf(getSystemViewsIds), - "GET", - "/system-views-ids", - "Get Ids of System Views", - s"""Get Ids of System Views - | - |${userAuthenticationMessage(true)} - | - """.stripMargin, - EmptyBody, - viewIdsJsonV500, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UnknownError - ), - List(apiTagSystemView), - Some(List(canGetSystemView)) - ) - - lazy val getSystemViewsIds: OBPEndpoint = { - case "system-views-ids" :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - views <- ViewNewStyle.systemViews() - } yield { - (createViewsIdsJsonV500(views), HttpCode.`200`(cc.callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - createSystemView, - implementedInApiVersion, - nameOf(createSystemView), - "POST", - "/system-views", - "Create System View", - s"""Create a system view - | - | ${userAuthenticationMessage(true)} and the user needs to have access to the $canCreateSystemView entitlement. - | - | The 'allowed_actions' field is a list containing the names of the actions allowed through this view. - | All the actions contained in the list will be set to `true` on the view creation, the rest will be set to `false`. - | - | The 'alias' field in the JSON can take one of three values: - | - | * _public_: to use the public alias if there is one specified for the other account. - | * _private_: to use the private alias if there is one specified for the other account. - | * _''(empty string)_: to use no alias; the view shows the real name of the other account. - | - | The 'hide_metadata_if_alias_used' field in the JSON can take boolean values. If it is set to `true` and there is an alias on the other account then the other accounts' metadata (like more_info, url, image_url, open_corporates_url, etc.) will be hidden. Otherwise the metadata will be shown. - | - | The 'metadata_view' field determines where metadata (comments, tags, images, where tags) for transactions are stored and retrieved. If set to another view's ID (e.g. 'owner'), metadata added through this view will be shared with all other views that also use the same metadata_view value. If left empty, metadata is stored under this view's own ID and is not shared with other views. - | - | System views cannot be public. In case you try to set it you will get the error $SystemViewCannotBePublicError - | """, - createSystemViewJsonV500, - viewJsonV500, - List( - $AuthenticatedUserIsRequired, - InvalidJsonFormat, - UnknownError - ), - List(apiTagSystemView), - Some(List(canCreateSystemView)) - ) - - lazy val createSystemView : OBPEndpoint = { - //creates a system view - case "system-views" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - createViewJson <- NewStyle.function.tryons(failMsg = s"$InvalidJsonFormat The Json body should be the $CreateViewJson ", 400, cc.callContext) { - json.extract[CreateViewJsonV500] - } - _ <- Helper.booleanToFuture(SystemViewCannotBePublicError, failCode=400, cc=cc.callContext) { - createViewJson.is_public == false - } - // custom views are started with `_`,eg _ life, _ work, and System views can not, eg: owner. - _ <- Helper.booleanToFuture(failMsg = InvalidSystemViewFormat +s"Current view_name (${createViewJson.name})", cc = cc.callContext) { - isValidSystemViewName(createViewJson.name) - } - view <- ViewNewStyle.createSystemView(createViewJson.toCreateViewJson, cc.callContext) - } yield { - (createViewJsonV500(view), HttpCode.`201`(cc.callContext)) - } - } - } - - - staticResourceDocs += ResourceDoc( - updateSystemView, - implementedInApiVersion, - nameOf(updateSystemView), - "PUT", - "/system-views/VIEW_ID", - "Update System View", - s"""Update an existing view on a bank account - | - |${userAuthenticationMessage(true)} and the user needs to have access to the owner view. - | - |The json sent is the same as during view creation (above), with one difference: the 'name' field - |of a view is not editable (it is only set when a view is created)""", - updateSystemViewJson500, - viewJsonV500, - List( - InvalidJsonFormat, - $AuthenticatedUserIsRequired, - BankAccountNotFound, - UnknownError - ), - List(apiTagSystemView), - Some(List(canUpdateSystemView)) - ) - - lazy val updateSystemView : OBPEndpoint = { - //updates a view on a bank account - case "system-views" :: viewId :: Nil JsonPut json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - updateJson <- Future { tryo{json.extract[UpdateViewJsonV500]} } map { - val msg = s"$InvalidJsonFormat The Json body should be the $UpdateViewJSON " - x => unboxFullOrFail(x, cc.callContext, msg) - } - _ <- Helper.booleanToFuture(SystemViewCannotBePublicError, failCode=400, cc=cc.callContext) { - updateJson.is_public == false - } - _ <- ViewNewStyle.systemView(ViewId(viewId), cc.callContext) - updatedView <- ViewNewStyle.updateSystemView(ViewId(viewId), updateJson.toUpdateViewJson, cc.callContext) - } yield { - (createViewJsonV500(updatedView), HttpCode.`200`(cc.callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - createCustomerAccountLink, - implementedInApiVersion, - nameOf(createCustomerAccountLink), - "POST", - "/banks/BANK_ID/customer-account-links", - "Create Customer Account Link", - s"""Link a Customer to a Account - | - |${userAuthenticationMessage(true)} - | - |""", - createCustomerAccountLinkJson, - customerAccountLinkJson, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - BankAccountNotFound, - InvalidJsonFormat, - CustomerNotFoundByCustomerId, - UserHasMissingRoles, - AccountAlreadyExistsForCustomer, - CreateCustomerAccountLinkError, - UnknownError - ), - List(apiTagCustomer, apiTagAccount), - Some(List(canCreateCustomerAccountLink))) - lazy val createCustomerAccountLink : OBPEndpoint = { - case "banks" :: BankId(bankId):: "customer-account-links" :: Nil JsonPost json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, _,callContext) <- SS.userBank - - postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $CreateCustomerAccountLinkJson ", 400, callContext) { - json.extract[CreateCustomerAccountLinkJson] - } - (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(postedData.customer_id, callContext) - _ <- booleanToFuture(s"Bank of the customer specified by the CUSTOMER_ID(${customer.bankId}) has to matches BANK_ID(${bankId.value}) in URL", 400, callContext) { - customer.bankId == bankId.value - } - (_, callContext) <- NewStyle.function.getBankAccount(bankId, AccountId(postedData.account_id), callContext) - _ <- booleanToFuture("Field customer_id is not defined in the posted json!", 400, callContext) { - postedData.customer_id.nonEmpty - } - (customerAccountLinkExists, callContext) <- Connector.connector.vend.getCustomerAccountLink(postedData.customer_id, postedData.account_id, callContext) - _ <- booleanToFuture(AccountAlreadyExistsForCustomer, 400, callContext) { - customerAccountLinkExists.isEmpty - } - (customerAccountLink, callContext) <- NewStyle.function.createCustomerAccountLink(postedData.customer_id, postedData.bank_id, postedData.account_id, postedData.relationship_type, callContext) - } yield { - (JSONFactory500.createCustomerAccountLinkJson(customerAccountLink), HttpCode.`201`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getCustomerAccountLinksByCustomerId, - implementedInApiVersion, - nameOf(getCustomerAccountLinksByCustomerId), - "GET", - "/banks/BANK_ID/customers/CUSTOMER_ID/customer-account-links", - "Get Customer Account Links by CUSTOMER_ID", - s""" Get Customer Account Links by CUSTOMER_ID - | - |${userAuthenticationMessage(true)} - | - |""", - EmptyBody, - customerAccountLinksJson, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - CustomerNotFoundByCustomerId, - UserHasMissingRoles, - UnknownError - ), - List(apiTagCustomer), - Some(List(canGetCustomerAccountLinks))) - lazy val getCustomerAccountLinksByCustomerId : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customers" :: customerId :: "customer-account-links" :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, cc.callContext) - _ <- booleanToFuture(s"Bank of the customer specified by the CUSTOMER_ID(${customer.bankId}) has to matches BANK_ID(${bankId.value}) in URL", 400, callContext) { - customer.bankId == bankId.value - } - (customerAccountLinks, callContext) <- NewStyle.function.getCustomerAccountLinksByCustomerId(customerId, callContext) - } yield { - (JSONFactory500.createCustomerAccountLinksJon(customerAccountLinks), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getCustomerAccountLinksByBankIdAccountId, - implementedInApiVersion, - nameOf(getCustomerAccountLinksByBankIdAccountId), - "GET", - "/banks/BANK_ID/accounts/ACCOUNT_ID/customer-account-links", - "Get Customer Account Links by ACCOUNT_ID", - s""" Get Customer Account Links by ACCOUNT_ID - | - |${userAuthenticationMessage(true)} - | - |""", - EmptyBody, - customerAccountLinksJson, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - BankAccountNotFound, - UserHasMissingRoles, - UnknownError - ), - List(apiTagCustomer), - Some(List(canGetCustomerAccountLinks))) - lazy val getCustomerAccountLinksByBankIdAccountId : OBPEndpoint = { - case "banks" :: bankId :: "accounts" :: accountId :: "customer-account-links" :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, _,callContext) <- SS.userBank - (customerAccountLinks, callContext) <- NewStyle.function.getCustomerAccountLinksByBankIdAccountId(bankId, accountId, callContext) - } yield { - (JSONFactory500.createCustomerAccountLinksJon(customerAccountLinks), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - getCustomerAccountLinkById, - implementedInApiVersion, - nameOf(getCustomerAccountLinkById), - "GET", - "/banks/BANK_ID/customer-account-links/CUSTOMER_ACCOUNT_LINK_ID", - "Get Customer Account Link by Id", - s""" Get Customer Account Link by CUSTOMER_ACCOUNT_LINK_ID - | - |${userAuthenticationMessage(true)} - | - |""", - EmptyBody, - customerAccountLinkJson, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UserHasMissingRoles, - UnknownError - ), - List(apiTagCustomer), - Some(List(canGetCustomerAccountLink))) - lazy val getCustomerAccountLinkById : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customer-account-links" :: customerAccountLinkId :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, _,callContext) <- SS.userBank - (customerAccountLink, callContext) <- NewStyle.function.getCustomerAccountLinkById(customerAccountLinkId, callContext) - } yield { - (JSONFactory500.createCustomerAccountLinkJson(customerAccountLink), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - updateCustomerAccountLinkById, - implementedInApiVersion, - nameOf(updateCustomerAccountLinkById), - "PUT", - "/banks/BANK_ID/customer-account-links/CUSTOMER_ACCOUNT_LINK_ID", - "Update Customer Account Link by Id", - s""" Update Customer Account Link by CUSTOMER_ACCOUNT_LINK_ID - | - |${userAuthenticationMessage(true)} - | - |""", - updateCustomerAccountLinkJson, - customerAccountLinkJson, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UserHasMissingRoles, - UnknownError - ), - List(apiTagCustomer), - Some(List(canUpdateCustomerAccountLink))) - lazy val updateCustomerAccountLinkById : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customer-account-links" :: customerAccountLinkId :: Nil JsonPut json -> _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), _,callContext) <- SS.userBank - postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $UpdateCustomerAccountLinkJson ", 400, callContext) { - json.extract[UpdateCustomerAccountLinkJson] - } - (_, callContext) <- NewStyle.function.getCustomerAccountLinkById(customerAccountLinkId, callContext) - - (customerAccountLink, callContext) <- NewStyle.function.updateCustomerAccountLinkById(customerAccountLinkId, postedData.relationship_type, callContext) - } yield { - (JSONFactory500.createCustomerAccountLinkJson(customerAccountLink), HttpCode.`200`(callContext)) - } - } - } - - staticResourceDocs += ResourceDoc( - deleteCustomerAccountLinkById, - implementedInApiVersion, - nameOf(deleteCustomerAccountLinkById), - "DELETE", - "/banks/BANK_ID/customer-account-links/CUSTOMER_ACCOUNT_LINK_ID", - "Delete Customer Account Link", - s""" Delete Customer Account Link by CUSTOMER_ACCOUNT_LINK_ID - | - |${userAuthenticationMessage(true)} - | - |""", - EmptyBody, - EmptyBody, - List( - $AuthenticatedUserIsRequired, - $BankNotFound, - UserHasMissingRoles, - UnknownError - ), - List(apiTagCustomer), - Some(List(canDeleteCustomerAccountLink))) - lazy val deleteCustomerAccountLinkById : OBPEndpoint = { - case "banks" :: BankId(bankId) :: "customer-account-links" :: customerAccountLinkId :: Nil JsonDelete _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (Full(u), _,callContext) <- SS.userBank - (_, callContext) <- NewStyle.function.getCustomerAccountLinkById(customerAccountLinkId, callContext) - (deleted, callContext) <- NewStyle.function.deleteCustomerAccountLinkById(customerAccountLinkId, callContext) - } yield { - (Full(deleted), HttpCode.`204`(callContext)) - } - } - } - - resourceDocs += ResourceDoc( - getAdapterInfo, - implementedInApiVersion, - nameOf(getAdapterInfo), - "GET", - "/adapter", - "Get Adapter Info", - s"""Get basic information about the Adapter. - | - |${userAuthenticationMessage(true)} - | - """.stripMargin, - EmptyBody, - adapterInfoJsonV500, - List($AuthenticatedUserIsRequired, UserHasMissingRoles, UnknownError), - List(apiTagApi), - Some(List(canGetAdapterInfo)) - ) - lazy val getAdapterInfo: OBPEndpoint = { - case "adapter" :: Nil JsonGet _ => { - cc => implicit val ec = EndpointContext(Some(cc)) - for { - (_, callContext) <- SS.user - (adapterInfo,_) <- NewStyle.function.getAdapterInfo(callContext) - } yield { - (JSONFactory500.createAdapterInfoJson(adapterInfo,cc.startTime.getOrElse(Helpers.now).getTime), HttpCode.`200`(callContext)) - } - } - } - } -} - -object APIMethods500 extends RestHelper with APIMethods500 { - lazy val newStyleEndpoints: List[(String, String)] = Implementations5_0_0.resourceDocs.map { - rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString()) - }.toList +/** + * All v5.0.0 Lift endpoints have been migrated to Http4s500. + * This stub is kept so that existing test files that import + * APIMethods500.Implementations5_0_0 continue to compile. + */ +object APIMethods500 { + val Implementations5_0_0 = Http4s500.Implementations5_0_0 } +trait APIMethods500 + +// ─── Original Lift implementation (commented out) ──────────────────────────── +//package code.api.v5_0_0 +// +//import scala.language.reflectiveCalls +//import code.accountattribute.AccountAttributeX +//import code.api.Constant._ +//import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ +//import code.api.util.APIUtil._ +//import code.api.util.ApiRole._ +//import code.api.util.ApiTag._ +//import code.api.util.ErrorMessages._ +//import code.api.util.FutureUtil.EndpointContext +//import code.api.util.NewStyle.HttpCode +//import code.api.util.NewStyle.function.extractQueryParams +//import code.api.util._ +//import code.api.util.newstyle.ViewNewStyle +//import code.api.v2_1_0.JSONFactory210 +//import code.api.v3_0_0.JSONFactory300 +//import code.api.v3_1_0._ +//import code.api.v4_0_0.JSONFactory400.createCustomersMinimalJson +//import code.api.v4_0_0.{JSONFactory400, PostCounterpartyJson400} +//import code.api.v5_0_0.JSONFactory500.{createPhysicalCardJson, createViewJsonV500, createViewsIdsJsonV500, createViewsJsonV500} +//import code.api.v5_1_0.{CreateCustomViewJson, PostCounterpartyLimitV510, PostVRPConsentRequestJsonV510} +//import code.bankconnectors.Connector +//import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent} +//import code.consumer.Consumers +//import code.entitlement.Entitlement +//import code.metadata.counterparties.MappedCounterparty +//import code.metrics.APIMetrics +//import code.model.dataAccess.BankAccountCreation +//import code.util.Helper +//import code.util.Helper.{SILENCE_IS_GOLDEN, booleanToFuture} +//import code.views.Views +//import com.github.dwickern.macros.NameOf.nameOf +//import com.openbankproject.commons.ExecutionContext.Implicits.global +//import com.openbankproject.commons.model._ +//import com.openbankproject.commons.model.enums.StrongCustomerAuthentication +//import com.openbankproject.commons.util.ApiVersion +//import net.liftweb.common.{Empty, Full} +//import net.liftweb.http.Req +//import net.liftweb.http.rest.RestHelper +//import net.liftweb.json +//import net.liftweb.json.{Extraction, compactRender, prettyRender} +//import net.liftweb.mapper.By +//import net.liftweb.util.Helpers.tryo +//import net.liftweb.util.{Helpers, Props, StringHelpers} +// +//import java.util.UUID +//import java.util.concurrent.ThreadLocalRandom +//import scala.collection.immutable.{List, Nil} +//import scala.collection.mutable.ArrayBuffer +//import scala.concurrent.Future +//import scala.util.Random +// +//trait APIMethods500 { +// self: RestHelper => +// +// val Implementations5_0_0 = new Implementations500() +// +// protected trait TestHead { +// /** +// * Test to see if the request is a GET and expecting JSON in the response. +// * The path and the Req instance are extracted. +// */ +// def unapply(r: Req): Option[(List[String], Req)] = +// if (r.requestType.head_? && testResponse_?(r)) +// Some(r.path.partPath -> r) else None +// +// def testResponse_?(r: Req): Boolean +// } +// +// lazy val JsonHead = new TestHead with JsonTest +// +// class Implementations500 { +// +// val implementedInApiVersion = ApiVersion.v5_0_0 +// +// private val staticResourceDocs = ArrayBuffer[ResourceDoc]() +// def resourceDocs = staticResourceDocs +// +// val apiRelations = ArrayBuffer[ApiRelation]() +// val codeContext = CodeContext(staticResourceDocs, apiRelations) +// +// +// staticResourceDocs += ResourceDoc( +// root, +// implementedInApiVersion, +// "root", +// "GET", +// "/root", +// "Get API Info (root)", +// """Returns information about: +// | +// |* API version +// |* Hosted by information +// |* Hosted at information +// |* Energy source information +// |* Git Commit""", +// EmptyBody, +// apiInfoJson400, +// List(UnknownError, MandatoryPropertyIsNotSet), +// apiTagApi :: Nil) +// +// lazy val root: OBPEndpoint = { +// case (Nil | "root" :: Nil) JsonGet _ => { +// cc => +// implicit val ec = EndpointContext(Some(cc)) +// for { +// _ <- Future(()) // Just start async call +// } yield { +// (JSONFactory400.getApiInfoJSON(OBPAPI5_0_0.version,OBPAPI5_0_0.versionStatus), HttpCode.`200`(cc.callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getBank, +// implementedInApiVersion, +// nameOf(getBank), +// "GET", +// "/banks/BANK_ID", +// "Get Bank", +// """Get the bank specified by BANK_ID +// |Returns information about a single bank specified by BANK_ID including: +// | +// |* Bank code and full name of bank +// |* Logo URL +// |* Website""", +// EmptyBody, +// bankJson500, +// List(UnknownError, BankNotFound), +// apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil +// ) +// +// lazy val getBank : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (bank, callContext) <- NewStyle.function.getBank(bankId, cc.callContext) +// (attributes, callContext) <- NewStyle.function.getBankAttributesByBank(bankId, callContext) +// } yield +// (JSONFactory500.createBankJSON500(bank, attributes), HttpCode.`200`(callContext)) +// } +// } +// +// staticResourceDocs += ResourceDoc( +// createBank, +// implementedInApiVersion, +// "createBank", +// "POST", +// "/banks", +// "Create Bank", +// s"""Create a new bank (Authenticated access). +// | +// |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. +// |Thus the User can manage the bank they create and assign Roles to other Users. +// | +// |Only SANDBOX mode (i.e. when connector=mapped in properties file) +// |The settlement accounts are automatically created by the system when the bank is created. +// |Name and account id are created in accordance to the next rules: +// | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) +// | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) +// | +// |""", +// postBankJson500, +// bankJson500, +// List( +// InvalidJsonFormat, +// $AuthenticatedUserIsRequired, +// InsufficientAuthorisationToCreateBank, +// UnknownError +// ), +// List(apiTagBank), +// Some(List(canCreateBank)) +// ) +// +// lazy val createBank: OBPEndpoint = { +// case "banks" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson500 " +// for { +// postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { +// json.extract[PostBankJson500] +// } +// +// //if postJson.id is empty, just return SILENCE_IS_GOLDEN, and will pass the guard. +// checkShortStringValue = APIUtil.checkOptionalShortString(postJson.id.getOrElse(SILENCE_IS_GOLDEN)) +// _ <- Helper.booleanToFuture(failMsg = s"$checkShortStringValue.", cc = cc.callContext) { +// checkShortStringValue == SILENCE_IS_GOLDEN +// } +// +// _ <- Helper.booleanToFuture(failMsg = ErrorMessages.InvalidConsumerCredentials, cc=cc.callContext) { +// cc.callContext.map(_.consumer.isDefined == true).isDefined +// } +// _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat Min length of BANK_ID should be greater than 3 characters.", cc=cc.callContext) { +// postJson.id.forall(_.length > 3) +// } +// _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain space characters", cc=cc.callContext) { +// !postJson.id.contains(" ") +// } +// _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain `::::` characters", cc=cc.callContext) { +// !`checkIfContains::::`(postJson.id.getOrElse("")) +// } +// (banks, callContext) <- NewStyle.function.getBanks(cc.callContext) +// _ <- Helper.booleanToFuture(failMsg = ErrorMessages.bankIdAlreadyExists, cc=cc.callContext) { +// !banks.exists { b => Some(b.bankId.value) == postJson.id } +// } +// (success, callContext) <- NewStyle.function.createOrUpdateBank( +// postJson.id.getOrElse(APIUtil.generateUUID()), +// postJson.full_name.getOrElse(""), +// postJson.bank_code, +// postJson.logo.getOrElse(""), +// postJson.website.getOrElse(""), +// postJson.bank_routings.getOrElse(Nil).find(_.scheme == "BIC").map(_.address).getOrElse(""), +// "", +// postJson.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.scheme).getOrElse(""), +// postJson.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.address).getOrElse(""), +// callContext +// ) +// entitlements <- NewStyle.function.getEntitlementsByUserId(cc.userId, callContext) +// entitlementsByBank = entitlements.filter(_.bankId==postJson.id.getOrElse("")) +// _ <- entitlementsByBank.filter(_.roleName == CanCreateEntitlementAtOneBank.toString()).size > 0 match { +// case true => +// // Already has entitlement +// Future(()) +// case false => +// Future(Entitlement.entitlement.vend.addEntitlement(postJson.id.getOrElse(""), cc.userId, CanCreateEntitlementAtOneBank.toString())) +// } +// _ <- entitlementsByBank.filter(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()).size > 0 match { +// case true => +// // Already has entitlement +// Future(()) +// case false => +// Future(Entitlement.entitlement.vend.addEntitlement(postJson.id.getOrElse(""), cc.userId, CanReadDynamicResourceDocsAtOneBank.toString())) +// } +// } yield { +// (JSONFactory500.createBankJSON500(success), HttpCode.`201`(callContext)) +// } +// } +// } +// staticResourceDocs += ResourceDoc( +// updateBank, +// implementedInApiVersion, +// "updateBank", +// "PUT", +// "/banks", +// "Update Bank", +// s"""Update an existing bank (Authenticated access). +// | +// |""", +// postBankJson500, +// bankJson500, +// List( +// InvalidJsonFormat, +// $AuthenticatedUserIsRequired, +// BankNotFound, +// updateBankError, +// UnknownError +// ), +// List(apiTagBank), +// Some(List(canCreateBank)) +// ) +// +// lazy val updateBank: OBPEndpoint = { +// case "banks" :: Nil JsonPut json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson500 " +// for { +// bank <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { +// json.extract[PostBankJson500] +// } +// _ <- Helper.booleanToFuture(failMsg = ErrorMessages.InvalidConsumerCredentials, cc=cc.callContext) { +// cc.callContext.map(_.consumer.isDefined == true).isDefined +// } +// _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat Min length of BANK_ID should be greater than 3 characters.", cc=cc.callContext) { +// bank.id.forall(_.length > 3) +// } +// _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain space characters", cc=cc.callContext) { +// !bank.id.contains(" ") +// } +// bankId <- NewStyle.function.tryons(ErrorMessages.updateBankError, 400, cc.callContext) { +// bank.id.get +// } +// (_, callContext) <- NewStyle.function.getBank(BankId(bankId), cc.callContext) +// (success, callContext) <- NewStyle.function.createOrUpdateBank( +// bankId, +// bank.full_name.getOrElse(""), +// bank.bank_code, +// bank.logo.getOrElse(""), +// bank.website.getOrElse(""), +// bank.bank_routings.getOrElse(Nil).find(_.scheme == "BIC").map(_.address).getOrElse(""), +// "", +// bank.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.scheme).getOrElse(""), +// bank.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.address).getOrElse(""), +// callContext +// ) +// } yield { +// (JSONFactory500.createBankJSON500(success), HttpCode.`200`(callContext)) +// } +// } +// } +// +// +// resourceDocs += ResourceDoc( +// createAccount, +// implementedInApiVersion, +// "createAccount", +// "PUT", +// "/banks/BANK_ID/accounts/ACCOUNT_ID", +// "Create Account (PUT)", +// """Create Account at bank specified by BANK_ID with Id specified by ACCOUNT_ID. +// | +// |The User can create an Account for themself - or - the User that has the USER_ID specified in the POST body. +// | +// |If the PUT body USER_ID *is* specified, the logged in user must have the Role canCreateAccount. Once created, the Account will be owned by the User specified by USER_ID. +// | +// |If the PUT body USER_ID is *not* specified, the account will be owned by the logged in User. +// | +// |The 'product_code' field SHOULD be a product_code from Product. +// |If the 'product_code' matches a product_code from Product, account attributes will be created that match the Product Attributes. +// | +// |Note: The Amount MUST be zero.""".stripMargin, +// createAccountRequestJsonV500, +// createAccountResponseJsonV310, +// List( +// InvalidJsonFormat, +// BankNotFound, +// AuthenticatedUserIsRequired, +// InvalidUserId, +// InvalidAccountIdFormat, +// InvalidBankIdFormat, +// UserNotFoundById, +// UserHasMissingRoles, +// InvalidAccountBalanceAmount, +// InvalidAccountInitialBalance, +// InitialBalanceMustBeZero, +// InvalidAccountBalanceCurrency, +// AccountIdAlreadyExists, +// UnknownError +// ), +// List(apiTagAccount,apiTagOnboarding), +// Some(List(canCreateAccount)) +// ) +// +// +// lazy val createAccount : OBPEndpoint = { +// // Create a new account +// case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonPut json -> _ => { +// cc =>{ +// implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), callContext) <- authenticatedAccess(cc) +// (account, callContext) <- Connector.connector.vend.checkBankAccountExists(bankId, accountId, callContext) +// _ <- Helper.booleanToFuture(AccountIdAlreadyExists, cc=callContext){ +// account.isEmpty +// } +// failMsg = s"$InvalidJsonFormat The Json body should be the ${prettyRender(Extraction.decompose(createAccountRequestJsonV310))} " +// createAccountJson <- NewStyle.function.tryons(failMsg, 400, callContext) { +// json.extract[CreateAccountRequestJsonV500] +// } +// loggedInUserId = u.userId +// userIdAccountOwner = createAccountJson.user_id match { +// case Some(userId) => userId +// case _ => loggedInUserId +// } +// _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext){ +// isValidID(accountId.value) +// } +// _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext){ +// isValidID(accountId.value) +// } +// (postedOrLoggedInUser,callContext) <- NewStyle.function.findByUserId(userIdAccountOwner, callContext) +// // User can create account for self or an account for another user if they have CanCreateAccount role +// _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext){ +// isValidID(accountId.value) +// } +// _ <- {userIdAccountOwner == loggedInUserId} match { +// case true => Future.successful(Full(Unit)) +// case false => +// NewStyle.function.hasEntitlement( +// bankId.value, +// loggedInUserId, +// canCreateAccount, +// callContext, +// s"${UserHasMissingRoles} $canCreateAccount or create account for self" +// ) +// } +// initialBalanceAsString = createAccountJson.balance.map(_.amount).getOrElse("0") +// accountType = createAccountJson.product_code +// accountLabel = createAccountJson.label +// initialBalanceAsNumber <- NewStyle.function.tryons(InvalidAccountInitialBalance, 400, callContext) { +// BigDecimal(initialBalanceAsString) +// } +// _ <- Helper.booleanToFuture(InitialBalanceMustBeZero, cc=callContext){0 == initialBalanceAsNumber} +// _ <- Helper.booleanToFuture(InvalidISOCurrencyCode, cc=callContext){isValidCurrencyISOCode(createAccountJson.balance.map(_.currency).getOrElse("EUR"))} +// currency = createAccountJson.balance.map(_.currency).getOrElse("EUR") +// (_, callContext ) <- NewStyle.function.getBank(bankId, callContext) +// _ <- Helper.booleanToFuture(s"$InvalidAccountRoutings Duplication detected in account routings, please specify only one value per routing scheme", 400, cc=callContext){ +// createAccountJson.account_routings.getOrElse(Nil).map(_.scheme).distinct.size == createAccountJson.account_routings.getOrElse(Nil).size +// } +// alreadyExistAccountRoutings <- Future.sequence(createAccountJson.account_routings.getOrElse(Nil).map(accountRouting => +// NewStyle.function.getAccountRouting(Some(bankId), accountRouting.scheme, accountRouting.address, callContext).map(_ => Some(accountRouting)).fallbackTo(Future.successful(None)) +// )) +// alreadyExistingAccountRouting = alreadyExistAccountRoutings.collect { +// case Some(accountRouting) => s"bankId: $bankId, scheme: ${accountRouting.scheme}, address: ${accountRouting.address}" +// } +// _ <- Helper.booleanToFuture(s"$AccountRoutingAlreadyExist (${alreadyExistingAccountRouting.mkString("; ")})", cc=callContext) { +// alreadyExistingAccountRouting.isEmpty +// } +// (bankAccount,callContext) <- NewStyle.function.createBankAccount( +// bankId, +// accountId, +// accountType, +// accountLabel, +// currency, +// initialBalanceAsNumber, +// postedOrLoggedInUser.name, +// createAccountJson.branch_id.getOrElse(""), +// createAccountJson.account_routings.getOrElse(Nil).map(r => AccountRouting(r.scheme, r.address)), +// callContext +// ) +// (productAttributes, callContext) <- NewStyle.function.getProductAttributesByBankAndCode(bankId, ProductCode(accountType), callContext) +// (accountAttributes, callContext) <- NewStyle.function.createAccountAttributes( +// bankId, +// accountId, +// ProductCode(accountType), +// productAttributes, +// None, +// callContext: Option[CallContext] +// ) +// //1 Create or Update the `Owner` for the new account +// //2 Add permission to the user +// //3 Set the user as the account holder +// _ <- BankAccountCreation.setAccountHolderAndRefreshUserAccountAccess(bankId, accountId, postedOrLoggedInUser, callContext) +// } yield { +// (JSONFactory310.createAccountJSON(userIdAccountOwner, bankAccount, accountAttributes), HttpCode.`201`(callContext)) +// } +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// createUserAuthContext, +// implementedInApiVersion, +// nameOf(createUserAuthContext), +// "POST", +// "/users/USER_ID/auth-context", +// "Create User Auth Context", +// s"""Create User Auth Context. These key value pairs will be propagated over connector to adapter. Normally used for mapping OBP user and +// | Bank User/Customer. +// |${userAuthenticationMessage(true)} +// |""", +// postUserAuthContextJson, +// userAuthContextJsonV500, +// List( +// AuthenticatedUserIsRequired, +// InvalidJsonFormat, +// CreateUserAuthContextError, +// UnknownError +// ), +// List(apiTagUser), +// Some(List(canCreateUserAuthContext))) +// lazy val createUserAuthContext : OBPEndpoint = { +// case "users" :: userId ::"auth-context" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), callContext) <- authenticatedAccess(cc) +// _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateUserAuthContext, callContext) +// failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextJson " +// postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { +// json.extract[PostUserAuthContextJson] +// } +// (user, callContext) <- NewStyle.function.findByUserId(userId, callContext) +// (userAuthContext, callContext) <- NewStyle.function.createUserAuthContext(user, postedData.key.trim, postedData.value.trim, callContext) +// } yield { +// (JSONFactory500.createUserAuthContextJson(userAuthContext), HttpCode.`201`(callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// getUserAuthContexts, +// implementedInApiVersion, +// nameOf(getUserAuthContexts), +// "GET", +// "/users/USER_ID/auth-context", +// "Get User Auth Contexts", +// s"""Get User Auth Contexts for a User. +// | +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// EmptyBody, +// userAuthContextJsonV500, +// List( +// AuthenticatedUserIsRequired, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagUser), +// Some(canGetUserAuthContext :: Nil) +// ) +// lazy val getUserAuthContexts : OBPEndpoint = { +// case "users" :: userId :: "auth-context" :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), callContext) <- authenticatedAccess(cc) +// _ <- NewStyle.function.hasEntitlement("", u.userId, canGetUserAuthContext, callContext) +// (_, callContext) <- NewStyle.function.findByUserId(userId, callContext) +// (userAuthContexts, callContext) <- NewStyle.function.getUserAuthContexts(userId, callContext) +// } yield { +// (JSONFactory500.createUserAuthContextsJson(userAuthContexts), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// createUserAuthContextUpdateRequest, +// implementedInApiVersion, +// nameOf(createUserAuthContextUpdateRequest), +// "POST", +// "/banks/BANK_ID/users/current/auth-context-updates/SCA_METHOD", +// "Create User Auth Context Update Request", +// s"""Create User Auth Context Update Request. +// |${userAuthenticationMessage(true)} +// | +// |A One Time Password (OTP) (AKA security challenge) is sent Out of Band (OOB) to the User via the transport defined in SCA_METHOD +// |SCA_METHOD is typically "SMS" or "EMAIL". "EMAIL" is used for testing purposes. +// | +// |""", +// postUserAuthContextJson, +// userAuthContextUpdateJsonV500, +// List( +// AuthenticatedUserIsRequired, +// $BankNotFound, +// InvalidJsonFormat, +// CreateUserAuthContextError, +// UnknownError +// ), +// List(apiTagUser), +// None +// ) +// +// lazy val createUserAuthContextUpdateRequest : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "users" :: "current" ::"auth-context-updates" :: scaMethod :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(user), callContext) <- authenticatedAccess(cc) +// _ <- Helper.booleanToFuture(failMsg = ConsumerHasMissingRoles + CanCreateUserAuthContextUpdate, cc=callContext) { +// checkScope(bankId.value, getConsumerPrimaryKey(callContext), ApiRole.canCreateUserAuthContextUpdate) +// } +// _ <- Helper.booleanToFuture(UserAuthContextUpdateRequestAllowedScaMethods, cc=callContext){ +// List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString()).exists(_ == scaMethod) +// } +// failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextJson " +// postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { +// json.extract[PostUserAuthContextJson] +// } +// (userAuthContextUpdate, callContext) <- NewStyle.function.validateUserAuthContextUpdateRequest(bankId.value, user.userId, postedData.key.trim, postedData.value.trim, scaMethod, callContext) +// } yield { +// +// (JSONFactory500.createUserAuthContextUpdateJson(userAuthContextUpdate), HttpCode.`201`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// answerUserAuthContextUpdateChallenge, +// implementedInApiVersion, +// nameOf(answerUserAuthContextUpdateChallenge), +// "POST", +// "/banks/BANK_ID/users/current/auth-context-updates/AUTH_CONTEXT_UPDATE_ID/challenge", +// "Answer User Auth Context Update Challenge", +// s""" +// |Answer User Auth Context Update Challenge. +// |""", +// postUserAuthContextUpdateJsonV310, +// userAuthContextUpdateJsonV500, +// List( +// AuthenticatedUserIsRequired, +// $BankNotFound, +// InvalidJsonFormat, +// InvalidConnectorResponse, +// UnknownError +// ), +// apiTagUser :: Nil) +// +// lazy val answerUserAuthContextUpdateChallenge : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "users" :: "current" ::"auth-context-updates" :: authContextUpdateId :: "challenge" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, callContext) <- authenticatedAccess(cc) +// failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextUpdateJsonV310 " +// postUserAuthContextUpdateJson <- NewStyle.function.tryons(failMsg, 400, callContext) { +// json.extract[PostUserAuthContextUpdateJsonV310] +// } +// (userAuthContextUpdate, callContext) <- NewStyle.function.checkAnswer(authContextUpdateId, postUserAuthContextUpdateJson.answer, callContext) +// (user, callContext) <- NewStyle.function.getUserByUserId(userAuthContextUpdate.userId, callContext) +// (_, callContext) <- +// userAuthContextUpdate.status match { +// case status if status == UserAuthContextUpdateStatus.ACCEPTED.toString => +// NewStyle.function.createUserAuthContext( +// user, +// userAuthContextUpdate.key.trim, +// userAuthContextUpdate.value.trim, +// callContext).map(x => (Some(x._1), x._2)) +// case _ => +// Future((None, callContext)) +// } +// (_, callContext) <- +// userAuthContextUpdate.key match { +// case "CUSTOMER_NUMBER" => +// NewStyle.function.getOCreateUserCustomerLink( +// bankId, +// userAuthContextUpdate.value, // Customer number +// user.userId, +// callContext +// ) +// case _ => +// Future((None, callContext)) +// } +// } yield { +// (JSONFactory500.createUserAuthContextUpdateJson(userAuthContextUpdate), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// createConsentRequest, +// implementedInApiVersion, +// nameOf(createConsentRequest), +// "POST", +// "/consumer/consent-requests", +// "Create Consent Request", +// s""" +// |Create a Consent Request — the first step of the OBP Consent flow. +// | +// |The calling application (TPP) authenticates with Client Credentials and posts the consent details (entitlements, account_access, VRP fields, etc.). The User then completes the flow by calling Create Consent By CONSENT_REQUEST_ID. +// | +// |The Consent Request is recorded against the calling consumer (its creator). +// | +// |Optional body fields of note: +// | +// |- `consumer_id`: if set, the resulting Consent (created when the User answers this request) will be pinned to this consumer instead of the creator. Use only when the consent is intended for a different application than the one creating the request. Most TPPs should omit this field — when omitted, the resulting Consent is pinned to the creator. Note: this override is being deprecated; v6.0.0 will pin the resulting Consent to the creator unconditionally. +// |- `email` / `phone_number`: surface in the SCA challenge if the User chooses EMAIL or SMS at answer time. +// |- `valid_from` / `time_to_live`: control the lifetime of the resulting Consent. +// | +// |Authentication: +// | +// |The client needs to authenticate themselves for this request. In case of public client we use client_id and private key to obtain access token; otherwise we use client_id and client_secret. The obtained access token is used in the HTTP Bearer auth header of our request. +// | +// |Example: +// |Authorization: Bearer eXtneO-THbQtn3zvK_kQtXXfvOZyZFdBCItlPDbR2Bk.dOWqtXCtFX-tqGTVR0YrIjvAolPIVg7GZ-jz83y6nA0 +// | +// |After successfully creating the Consent Request, call Create Consent By CONSENT_REQUEST_ID to finalize. +// | +// |See Glossary entry "Authentication: Consent OBP Flow Example" for an end-to-end walk-through. +// | +// |${applicationAccessMessage(true)} +// | +// |${userAuthenticationMessage(false)} +// | +// | +// |""".stripMargin, +// postConsentRequestJsonV500, +// consentRequestResponseJson, +// List( +// InvalidJsonFormat, +// ConsentMaxTTL, +// X509CannotGetCertificate, +// X509GeneralError, +// InvalidConnectorResponse, +// UnknownError +// ), +// apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil +// ) +// +// lazy val createConsentRequest : OBPEndpoint = { +// case "consumer" :: "consent-requests" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, callContext) <- applicationAccess(cc) +// _ <- passesPsd2Aisp(callContext) +// failMsg = s"$InvalidJsonFormat The Json body should be the $PostConsentBodyCommonJson " +// consentJson: PostConsentRequestJsonV500 <- NewStyle.function.tryons(failMsg, 400, callContext) { +// json.extract[PostConsentRequestJsonV500] +// } +// maxTimeToLive = APIUtil.getPropsAsIntValue(nameOfProperty="consents.max_time_to_live", defaultValue=3600) +// _ <- Helper.booleanToFuture(s"$ConsentMaxTTL ($maxTimeToLive)", cc=callContext){ +// consentJson.time_to_live match { +// case Some(ttl) => ttl <= maxTimeToLive +// case _ => true +// } +// } +// createdConsentRequest <- Future(ConsentRequests.consentRequestProvider.vend.createConsentRequest( +// callContext.flatMap(_.consumer), +// Some(compactRender(json)) +// )) map { +// i => connectorEmptyResponse(i, callContext) +// } +// } yield { +// (JSONFactory500.createConsentRequestResponseJson(createdConsentRequest), HttpCode.`201`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getConsentRequest, +// implementedInApiVersion, +// nameOf(getConsentRequest), +// "GET", +// "/consumer/consent-requests/CONSENT_REQUEST_ID", +// "Get Consent Request", +// s""" +// |Return the full payload of a previously-created Consent Request — the JSON the TPP submitted to Create Consent Request, plus the consent_request_id and the creating consumer_id. +// | +// |Use this endpoint to verify the contents of a Consent Request before the User answers it. +// | +// |Authentication: Application access (any registered consumer/application can read any Consent Request by ID). +// | +// |Note: this endpoint will be restricted to the creating consumer in v6.0.0. Until then, treat CONSENT_REQUEST_IDs as sensitive — they reveal entitlements, account routings, and contact details (email/phone) submitted at creation. +// | +// |""".stripMargin, +// EmptyBody, +// consentRequestResponseJson, +// List( +// InvalidJsonFormat, +// ConsentMaxTTL, +// X509CannotGetCertificate, +// X509GeneralError, +// InvalidConnectorResponse, +// UnknownError +// ), +// apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil +// ) +// +// lazy val getConsentRequest : OBPEndpoint = { +// case "consumer" :: "consent-requests" :: consentRequestId :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, callContext) <- applicationAccess(cc) +// _ <- passesPsd2Aisp(callContext) +// createdConsentRequest <- Future(ConsentRequests.consentRequestProvider.vend.getConsentRequestById( +// consentRequestId +// )) map { +// i => unboxFullOrFail(i,callContext, ConsentRequestNotFound) +// } +// } yield { +// (JSONFactory500.createConsentRequestResponseJson(createdConsentRequest), HttpCode.`200`(callContext) +// ) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getConsentByConsentRequestId, +// implementedInApiVersion, +// nameOf(getConsentByConsentRequestId), +// "GET", +// "/consumer/consent-requests/CONSENT_REQUEST_ID/consents", +// "Get Consent By Consent Request Id via Consumer", +// s""" +// | +// |This endpoint gets the Consent By consent request id. +// | +// |${userAuthenticationMessage(true)} +// | +// """.stripMargin, +// EmptyBody, +// consentJsonV500, +// List( +// $AuthenticatedUserIsRequired, +// UnknownError +// ), +// List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2)) +// lazy val getConsentByConsentRequestId: OBPEndpoint = { +// case "consumer" :: "consent-requests" :: consentRequestId :: "consents" :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, callContext) <- applicationAccess(cc) +// consent <- Future { Consents.consentProvider.vend.getConsentByConsentRequestId(consentRequestId)} map { +// unboxFullOrFail(_, callContext, ConsentRequestNotFound) +// } +// _ <- Helper.booleanToFuture(failMsg = ConsentNotFound, failCode = 404, cc = cc.callContext) { +// consent.mConsumerId.get == cc.consumer.map(_.consumerId.get).getOrElse("None") +// } +// (bankId, accountId, viewId, helperInfo) <- NewStyle.function.tryons(failMsg = Oauth2BadJWTException, 400, callContext) { +// val jsonWebTokenAsJValue = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(json.parse(_).extract[ConsentJWT]) +// val viewsFromJwtToken = jsonWebTokenAsJValue.head.views +// //at the moment,we only support VRP consent to show `ConsentAccountAccessJson` in the response. Because the TPP need them for payments. +// val isVrpConsent = (viewsFromJwtToken.length == 1 )&& (viewsFromJwtToken.head.bank_id.nonEmpty)&& (viewsFromJwtToken.head.account_id.nonEmpty)&& (viewsFromJwtToken.head.view_id.startsWith("_vrp-")) +// +// if(isVrpConsent){ +// val bankId = BankId(viewsFromJwtToken.head.bank_id) +// val accountId = AccountId(viewsFromJwtToken.head.account_id) +// val viewId = ViewId(viewsFromJwtToken.head.view_id) +// val helperInfoFromJwtToken = viewsFromJwtToken.head.helper_info +// val viewCanGetCounterparty = Views.views.vend.customView(viewId, BankIdAccountId(bankId, accountId)).map(_.allowed_actions.exists( _ == CAN_GET_COUNTERPARTY)) +// val helperInfo = if(viewCanGetCounterparty==Full(true)) helperInfoFromJwtToken else None +// (Some(bankId), Some(accountId), Some(viewId), helperInfo) +// }else{ +// (None, None, None, None) +// } +// } +// } yield { +// ( +// ConsentJsonV500( +// consent.consentId, +// consent.jsonWebToken, +// consent.status, +// Some(consent.consentRequestId), +// if (bankId.isDefined && accountId.isDefined && viewId.isDefined) { +// Some(ConsentAccountAccessJson( +// bank_id = bankId.get.value, +// account_id = accountId.get.value, +// view_id = viewId.get.value, +// helper_info = helperInfo +// )) +// } else { +// None +// } +// ), +// HttpCode.`200`(cc) +// ) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// createConsentByConsentRequestIdEmail, +// implementedInApiVersion, +// nameOf(createConsentByConsentRequestIdEmail), +// "POST", +// "/consumer/consent-requests/CONSENT_REQUEST_ID/EMAIL/consents", +// "Create Consent By CONSENT_REQUEST_ID (EMAIL)", +// s""" +// |Answer a Consent Request and create the resulting Consent, with an EMAIL Strong Customer Authentication challenge. +// | +// |After the TPP has called Create Consent Request (Client Credentials), the User authenticates and answers the request via this endpoint. This creates the Consent (the credential the consumer will use to access OBP on the User's behalf). +// | +// |An SCA challenge code is sent to the email address that was supplied in the Create Consent Request body. The User then completes SCA via Answer Consent Challenge, which moves the Consent from INITIATED to ACCEPTED. +// | +// |Pinning: the resulting Consent is pinned to a single consumer at creation. The pinned consumer is taken from the `consumer_id` field of the original Create Consent Request body if present, otherwise from the consumer that created the Request. After creation, only that consumer can present the resulting Consent JWT — any other consumer presenting it gets ConsentNotFound (consumer mismatch). +// | +// |Each Consent Request can be answered exactly once. A second call returns ConsentRequestIsInvalid. +// | +// |The Consent's authority is bounded by the answering User's own entitlements — it cannot grant access beyond what that User already has. +// | +// |Authentication: Any authenticated User may answer a Consent Request whose CONSENT_REQUEST_ID they know. This will be tightened in v6.0.0; until then, treat CONSENT_REQUEST_IDs as sensitive. +// | +// |""", +// EmptyBody, +// consentJsonV500, +// List( +// AuthenticatedUserIsRequired, +// BankNotFound, +// InvalidJsonFormat, +// ConsentAllowedScaMethods, +// RolesAllowedInConsent, +// ViewsAllowedInConsent, +// ConsumerNotFoundByConsumerId, +// ConsumerIsDisabled, +// InvalidConnectorResponse, +// UnknownError +// ), +// apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: apiTagVrp :: Nil) +// +// staticResourceDocs += ResourceDoc( +// createConsentByConsentRequestIdSms, +// implementedInApiVersion, +// nameOf(createConsentByConsentRequestIdSms), +// "POST", +// "/consumer/consent-requests/CONSENT_REQUEST_ID/SMS/consents", +// "Create Consent By CONSENT_REQUEST_ID (SMS)", +// s""" +// |Answer a Consent Request and create the resulting Consent, with an SMS Strong Customer Authentication challenge. +// | +// |After the TPP has called Create Consent Request (Client Credentials), the User authenticates and answers the request via this endpoint. This creates the Consent (the credential the consumer will use to access OBP on the User's behalf). +// | +// |An SCA challenge code is sent to the phone number that was supplied in the Create Consent Request body. The User then completes SCA via Answer Consent Challenge, which moves the Consent from INITIATED to ACCEPTED. +// | +// |Pinning: the resulting Consent is pinned to a single consumer at creation. The pinned consumer is taken from the `consumer_id` field of the original Create Consent Request body if present, otherwise from the consumer that created the Request. After creation, only that consumer can present the resulting Consent JWT — any other consumer presenting it gets ConsentNotFound (consumer mismatch). +// | +// |Each Consent Request can be answered exactly once. A second call returns ConsentRequestIsInvalid. +// | +// |The Consent's authority is bounded by the answering User's own entitlements — it cannot grant access beyond what that User already has. +// | +// |Authentication: Any authenticated User may answer a Consent Request whose CONSENT_REQUEST_ID they know. This will be tightened in v6.0.0; until then, treat CONSENT_REQUEST_IDs as sensitive. +// | +// |""", +// EmptyBody, +// consentJsonV500, +// List( +// AuthenticatedUserIsRequired, +// $BankNotFound, +// InvalidJsonFormat, +// ConsentRequestIsInvalid, +// ConsentAllowedScaMethods, +// RolesAllowedInConsent, +// ViewsAllowedInConsent, +// ConsumerNotFoundByConsumerId, +// ConsumerIsDisabled, +// MissingPropsValueAtThisInstance, +// SmsServerNotResponding, +// InvalidConnectorResponse, +// UnknownError +// ), +// apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil) +// +// staticResourceDocs += ResourceDoc( +// createConsentByConsentRequestIdImplicit, +// implementedInApiVersion, +// nameOf(createConsentByConsentRequestIdImplicit), +// "POST", +// "/consumer/consent-requests/CONSENT_REQUEST_ID/IMPLICIT/consents", +// "Create Consent By CONSENT_REQUEST_ID (IMPLICIT)", +// s""" +// |Answer a Consent Request and create the resulting Consent, without a Strong Customer Authentication challenge — the Consent is moved directly from INITIATED to ACCEPTED. +// | +// |After the TPP has called Create Consent Request (Client Credentials), the User authenticates and answers the request via this endpoint. This creates the Consent (the credential the consumer will use to access OBP on the User's behalf). +// | +// |IMPLICIT means no SCA challenge is sent. The Consent is immediately ACCEPTED. Use only in flows where the User has already been strongly authenticated by upstream means; for production use behind a public TPP, prefer EMAIL or SMS. +// | +// |Pinning: the resulting Consent is pinned to a single consumer at creation. The pinned consumer is taken from the `consumer_id` field of the original Create Consent Request body if present, otherwise from the consumer that created the Request. After creation, only that consumer can present the resulting Consent JWT — any other consumer presenting it gets ConsentNotFound (consumer mismatch). +// | +// |Each Consent Request can be answered exactly once. A second call returns ConsentRequestIsInvalid. +// | +// |The Consent's authority is bounded by the answering User's own entitlements — it cannot grant access beyond what that User already has. +// | +// |Authentication: Any authenticated User may answer a Consent Request whose CONSENT_REQUEST_ID they know. This will be tightened in v6.0.0; until then, treat CONSENT_REQUEST_IDs as sensitive. +// | +// |""", +// EmptyBody, +// consentJsonV500, +// List( +// AuthenticatedUserIsRequired, +// $BankNotFound, +// InvalidJsonFormat, +// ConsentRequestIsInvalid, +// ConsentAllowedScaMethods, +// RolesAllowedInConsent, +// ViewsAllowedInConsent, +// ConsumerNotFoundByConsumerId, +// ConsumerIsDisabled, +// MissingPropsValueAtThisInstance, +// SmsServerNotResponding, +// InvalidConnectorResponse, +// UnknownError +// ), +// apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil) +// +// lazy val createConsentByConsentRequestIdEmail = createConsentByConsentRequestId +// lazy val createConsentByConsentRequestIdSms = createConsentByConsentRequestId +// lazy val createConsentByConsentRequestIdImplicit = createConsentByConsentRequestId +// +// lazy val createConsentByConsentRequestId : OBPEndpoint = { +// +// case "consumer" :: "consent-requests":: consentRequestId :: scaMethod :: "consents" :: Nil JsonPost _ -> _ => { +// def sendEmailNotification(callContext: Option[CallContext], consentRequestJson: PostConsentRequestJsonV500, challengeText: String) = { +// for { +// failMsg <- Future { +// s"$InvalidJsonFormat The Json body must contain the field email" +// } +// consentScaEmail <- NewStyle.function.tryons(failMsg, 400, callContext) { +// consentRequestJson.email.head +// } +// (status, callContext) <- NewStyle.function.sendCustomerNotification( +// StrongCustomerAuthentication.EMAIL, +// consentScaEmail, +// Some("OBP Consent Challenge"), +// challengeText, +// callContext +// ) +// } yield { +// status +// } +// } +// def sendSmsNotification(callContext: Option[CallContext], consentRequestJson: PostConsentRequestJsonV500, challengeText: String) = { +// for { +// failMsg <- Future { +// s"$InvalidJsonFormat The Json body must contain the field phone_number" +// } +// consentScaPhoneNumber <- NewStyle.function.tryons(failMsg, 400, callContext) { +// consentRequestJson.phone_number.head +// } +// (status, callContext) <- NewStyle.function.sendCustomerNotification( +// StrongCustomerAuthentication.SMS, +// consentScaPhoneNumber, +// None, +// challengeText, +// callContext +// ) +// } yield { +// status +// } +// } +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(user), callContext) <- authenticatedAccess(cc) +// createdConsentRequest <- Future(ConsentRequests.consentRequestProvider.vend.getConsentRequestById( +// consentRequestId +// )) map { +// i => unboxFullOrFail(i,callContext, ConsentRequestNotFound) +// } +// _ <- Helper.booleanToFuture(s"$ConsentRequestIsInvalid, the current CONSENT_REQUEST_ID($consentRequestId) is already used to create a consent, please provide another one!", cc=callContext){ +// Consents.consentProvider.vend.getConsentByConsentRequestId(consentRequestId).isEmpty +// } +// _ <- Helper.booleanToFuture(ConsentAllowedScaMethods, cc=callContext){ +// List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString(), StrongCustomerAuthentication.IMPLICIT.toString()).exists(_ == scaMethod) +// } +// // If the payload contains "to_account` , it mean it is a VRP consent. +// isVrpConsent = createdConsentRequest.payload.contains("to_account") +// (consentRequestJson, isVRPConsentRequest) <- +// if(isVrpConsent) { +// val failMsg = s"$InvalidJsonFormat The vrp consent request json body should be the $PostVRPConsentRequestJsonV510 " +// NewStyle.function.tryons(failMsg, 400, callContext) { +// json.parse(createdConsentRequest.payload).extract[code.api.v5_1_0.PostVRPConsentRequestJsonInternalV510] +// }.map(postVRPConsentRequest => (postVRPConsentRequest.toPostConsentRequestJsonV500, true)) +// } else{ +// val failMsg = s"$InvalidJsonFormat The consent request Json body should be the $PostConsentRequestJsonV500 " +// NewStyle.function.tryons(failMsg, 400, callContext) { +// json.parse(createdConsentRequest.payload).extract[PostConsentRequestJsonV500] +// }.map(postVRPConsentRequest => (postVRPConsentRequest, false)) +// } +// +// //Here are all the VRP consent request +// (bankId, accountId, viewId, counterpartyId) <- if (isVRPConsentRequest) { +// val postConsentRequestJsonV510 = json.parse(createdConsentRequest.payload).extract[code.api.v5_1_0.PostVRPConsentRequestJsonV510] +// +// val vrpViewId = s"_vrp-${UUID.randomUUID.toString}".dropRight(5)// to make sure the length of the viewId is 36. +// val targetPermissions = List(//may need getTransactionRequest . so far only these payments. +// CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY, +// CAN_GET_COUNTERPARTY, +// CAN_SEE_TRANSACTION_REQUESTS, +// ) +// +// val targetCreateCustomViewJson = CreateCustomViewJson( +// name = vrpViewId, +// description = vrpViewId, +// metadata_view = vrpViewId, +// is_public = false, +// which_alias_to_use = vrpViewId, +// hide_metadata_if_alias_used = true, +// allowed_permissions = targetPermissions +// ) +// +// val fromBankAccountRoutings = BankAccountRoutings( +// bank = BankRoutingJson(postConsentRequestJsonV510.from_account.bank_routing.scheme, postConsentRequestJsonV510.from_account.bank_routing.address), +// account = BranchRoutingJsonV141(postConsentRequestJsonV510.from_account.account_routing.scheme, postConsentRequestJsonV510.from_account.account_routing.address), +// branch = AccountRoutingJsonV121(postConsentRequestJsonV510.from_account.branch_routing.scheme, postConsentRequestJsonV510.from_account.branch_routing.address) +// ) +// +// for { +// //1st: get the fromAccount by routings: +// (fromAccount, callContext) <- NewStyle.function.getBankAccountByRoutings(fromBankAccountRoutings, callContext) +// fromBankIdAccountId = BankIdAccountId(fromAccount.bankId, fromAccount.accountId) +// +// //2rd: create the Custom View for the fromAccount. +// //we do not need sourceViewId so far, we need to get all the view access for the login user, and +// permission <- NewStyle.function.permission(fromAccount.bankId, fromAccount.accountId, user, callContext) +// permissionsFromSource = permission.views.map(_.allowed_actions).flatten.toSet +// permissionsFromTarget = targetCreateCustomViewJson.allowed_permissions +// +// //eg: permissionsFromTarget=List(1,2), permissionsFromSource = List(1,3,4) => userMissingPermissions = List(2) +// //Here would find the missing permissions and show them in the error messages +// userMissingPermissions = permissionsFromTarget.toSet diff permissionsFromSource +// +// failMsg = s"${ErrorMessages.UserDoesNotHavePermission} ${userMissingPermissions.toString}" +// _ <- Helper.booleanToFuture(failMsg, cc = callContext) { +// userMissingPermissions.isEmpty +// } +// (vrpView, callContext) <- ViewNewStyle.createCustomView(fromBankIdAccountId, targetCreateCustomViewJson.toCreateViewJson, callContext) +// +// _ <-ViewNewStyle.grantAccessToCustomView(vrpView, user, callContext) +// +// //3rd: Create a new counterparty on that view (_VRP-9d429899-24f5-42c8-8565-943ffa6a7945) +// postJson = PostCounterpartyJson400( +// name = postConsentRequestJsonV510.to_account.counterparty_name, +// description = postConsentRequestJsonV510.to_account.counterparty_name, +// currency = postConsentRequestJsonV510.to_account.limit.currency, +// other_account_routing_scheme = StringHelpers.snakify(postConsentRequestJsonV510.to_account.account_routing.scheme).toUpperCase, +// other_account_routing_address = postConsentRequestJsonV510.to_account.account_routing.address, +// other_account_secondary_routing_scheme = "", +// other_account_secondary_routing_address = "", +// other_bank_routing_scheme = StringHelpers.snakify(postConsentRequestJsonV510.to_account.bank_routing.scheme).toUpperCase, +// other_bank_routing_address = postConsentRequestJsonV510.to_account.bank_routing.address, +// other_branch_routing_scheme = StringHelpers.snakify(postConsentRequestJsonV510.to_account.branch_routing.scheme).toUpperCase, +// other_branch_routing_address = postConsentRequestJsonV510.to_account.branch_routing.address, +// is_beneficiary = true, +// bespoke = Nil +// ) +// _ <- Helper.booleanToFuture(s"$InvalidValueLength. The maximum length of `description` field is ${MappedCounterparty.mDescription.maxLen}", cc = callContext) { +// postJson.description.length <= 36 +// } +// +// +// (counterparty, callContext) <- Connector.connector.vend.checkCounterpartyExists(postJson.name, fromBankIdAccountId.bankId.value, fromBankIdAccountId.accountId.value, vrpView.viewId.value, callContext) +// +// _ <- Helper.booleanToFuture(CounterpartyAlreadyExists.replace("value for BANK_ID or ACCOUNT_ID or VIEW_ID or NAME.", +// s"COUNTERPARTY_NAME(${postJson.name}) for the BANK_ID(${fromBankIdAccountId.bankId.value}) and ACCOUNT_ID(${fromBankIdAccountId.accountId.value}) and VIEW_ID($vrpViewId)"), cc = callContext) { +// counterparty.isEmpty +// } +// +// _ <- Helper.booleanToFuture(s"$InvalidISOCurrencyCode Current input is: '${postJson.currency}'", cc = callContext) { +// isValidCurrencyISOCode(postJson.currency) +// } +// +// (counterparty, callContext) <- NewStyle.function.createCounterparty( +// name = postJson.name, +// description = postJson.description, +// currency = postJson.currency, +// createdByUserId = user.userId, +// thisBankId = fromBankIdAccountId.bankId.value, +// thisAccountId = fromBankIdAccountId.accountId.value, +// thisViewId = vrpViewId, +// otherAccountRoutingScheme = postJson.other_account_routing_scheme, +// otherAccountRoutingAddress = postJson.other_account_routing_address, +// otherAccountSecondaryRoutingScheme = postJson.other_account_secondary_routing_scheme, +// otherAccountSecondaryRoutingAddress = postJson.other_account_secondary_routing_address, +// otherBankRoutingScheme = postJson.other_bank_routing_scheme, +// otherBankRoutingAddress = postJson.other_bank_routing_address, +// otherBranchRoutingScheme = postJson.other_branch_routing_scheme, +// otherBranchRoutingAddress = postJson.other_branch_routing_address, +// isBeneficiary = postJson.is_beneficiary, +// bespoke = postJson.bespoke.map(bespoke => CounterpartyBespoke(bespoke.key, bespoke.value)), +// callContext +// ) +// +// postCounterpartyLimitV510 = PostCounterpartyLimitV510( +// currency = postConsentRequestJsonV510.to_account.limit.currency, +// max_single_amount = postConsentRequestJsonV510.to_account.limit.max_single_amount, +// max_monthly_amount = postConsentRequestJsonV510.to_account.limit.max_monthly_amount, +// max_number_of_monthly_transactions = postConsentRequestJsonV510.to_account.limit.max_number_of_monthly_transactions, +// max_yearly_amount = postConsentRequestJsonV510.to_account.limit.max_yearly_amount, +// max_number_of_yearly_transactions = postConsentRequestJsonV510.to_account.limit.max_number_of_yearly_transactions, +// max_total_amount = postConsentRequestJsonV510.to_account.limit.max_total_amount, +// max_number_of_transactions = postConsentRequestJsonV510.to_account.limit.max_number_of_transactions +// ) +// +// //4th: create the counterparty limit +// (counterpartyLimitBox, callContext) <- Connector.connector.vend.getCounterpartyLimit( +// fromBankIdAccountId.bankId.value, +// fromBankIdAccountId.accountId.value, +// vrpViewId, +// counterparty.counterpartyId, +// cc.callContext +// ) +// failMsg = s"$CounterpartyLimitAlreadyExists Current BANK_ID(${fromBankIdAccountId.bankId.value}), " + +// s"ACCOUNT_ID(${fromBankIdAccountId.accountId.value}), VIEW_ID($vrpViewId),COUNTERPARTY_ID(${counterparty.counterpartyId})" +// _ <- Helper.booleanToFuture(failMsg, cc = callContext) { +// counterpartyLimitBox.isEmpty +// } +// (counterpartyLimit, callContext) <- NewStyle.function.createOrUpdateCounterpartyLimit( +// bankId = counterparty.thisBankId, +// accountId = counterparty.thisAccountId, +// viewId = counterparty.thisViewId, +// counterpartyId = counterparty.counterpartyId, +// postCounterpartyLimitV510.currency, +// BigDecimal(postCounterpartyLimitV510.max_single_amount), +// BigDecimal(postCounterpartyLimitV510.max_monthly_amount), +// postCounterpartyLimitV510.max_number_of_monthly_transactions, +// BigDecimal(postCounterpartyLimitV510.max_yearly_amount), +// postCounterpartyLimitV510.max_number_of_yearly_transactions, +// BigDecimal(postCounterpartyLimitV510.max_total_amount), +// postCounterpartyLimitV510.max_number_of_transactions, +// cc.callContext +// ) +// +// } yield { +// (fromAccount.bankId, fromAccount.accountId, vrpView.viewId, CounterpartyId(counterparty.counterpartyId)) +// } +// }else{ +// Future.successful(BankId(""), AccountId(""), ViewId(""),CounterpartyId("")) +// } +// +// maxTimeToLive = APIUtil.getPropsAsIntValue(nameOfProperty="consents.max_time_to_live", defaultValue=3600) +// _ <- Helper.booleanToFuture(s"$ConsentMaxTTL ($maxTimeToLive)", cc=callContext){ +// consentRequestJson.time_to_live match { +// case Some(ttl) => ttl <= maxTimeToLive +// case _ => true +// } +// } +// requestedEntitlements = consentRequestJson.entitlements.getOrElse(Nil) +// myEntitlements <- Entitlement.entitlement.vend.getEntitlementsByUserIdFuture(user.userId) +// _ <- Helper.booleanToFuture(RolesForbiddenInConsent, cc=callContext){ +// requestedEntitlements.map(_.role_name) +// .intersect( +// List( +// canCreateEntitlementAtOneBank.toString(), +// canCreateEntitlementAtAnyBank.toString()) +// ).length == 0 +// } +// _ <- Helper.booleanToFuture(RolesAllowedInConsent, cc=callContext){ +// requestedEntitlements.forall( +// re => myEntitlements.getOrElse(Nil).exists( +// e => e.roleName == re.role_name && e.bankId == re.bank_id +// ) +// ) +// } +// postConsentViewJsons <- if(isVrpConsent) { +// Future.successful(List(PostConsentViewJsonV310( +// bankId.value, +// accountId.value, +// viewId.value +// ))) +// }else{ +// Future.sequence( +// consentRequestJson.account_access.map( +// access => +// NewStyle.function.getBankAccountByRouting(consentRequestJson.bank_id.map(BankId(_)),access.account_routing.scheme, access.account_routing.address, cc.callContext) +// .map(result =>PostConsentViewJsonV310( +// result._1.bankId.value, +// result._1.accountId.value, +// access.view_id +// )) +// ) +// ) +// } +// +// (_, assignedViews) <- Future(Views.views.vend.privateViewsUserCanAccess(user)) +// _ <- Helper.booleanToFuture(ViewsAllowedInConsent, cc=callContext){ +// postConsentViewJsons.forall( +// rv => assignedViews.exists{ +// e => +// e.view_id == rv.view_id && +// e.bank_id == rv.bank_id && +// e.account_id == rv.account_id +// } +// ) +// } +// // Use consumer specified at the payload of consent request in preference to the field ConsumerId of consent request +// // i.e. ConsentRequest.Payload.consumer_id in preference to ConsentRequest.ConsumerId +// calculatedConsumerId = consentRequestJson.consumer_id.orElse(Some(createdConsentRequest.consumerId)) +// (consumerId, applicationText) <- calculatedConsumerId match { +// case Some(id) => NewStyle.function.checkConsumerByConsumerId(id, callContext) map { +// c => (Some(c.consumerId.get), c.description) +// } +// case None => Future(None, "Any application") +// } +// +// challengeAnswer = Props.mode match { +// case Props.RunModes.Test => Consent.challengeAnswerAtTestEnvironment +// case _ => SecureRandomUtil.numeric() +// } +// consumer = Consumers.consumers.vend.getConsumerByConsumerId(calculatedConsumerId.getOrElse("None")) +// createdConsent <- Future( +// Consents.consentProvider.vend.createObpConsent( +// user, +// challengeAnswer, +// Some(consentRequestId), +// consumer +// ) +// ) map { +// i => connectorEmptyResponse(i, callContext) +// } +// +// postConsentBodyCommonJson = PostConsentBodyCommonJson( +// everything = consentRequestJson.everything, +// bank_id = consentRequestJson.bank_id, +// views = postConsentViewJsons, +// entitlements = consentRequestJson.entitlements.getOrElse(Nil), +// consumer_id = consentRequestJson.consumer_id, +// consent_request_id = Some(consentRequestId), +// valid_from = consentRequestJson.valid_from, +// time_to_live = consentRequestJson.time_to_live, +// ) +// +// consentJWT = Consent.createConsentJWT( +// user, +// postConsentBodyCommonJson, +// createdConsent.secret, +// createdConsent.consentId, +// consumerId, +// postConsentBodyCommonJson.valid_from, +// postConsentBodyCommonJson.time_to_live.getOrElse(3600), +// Some(HelperInfoJson(List(counterpartyId.value))) +// ) +// _ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map { +// i => connectorEmptyResponse(i, callContext) +// } +// validUntil = Helper.calculateValidTo(postConsentBodyCommonJson.valid_from, postConsentBodyCommonJson.time_to_live.getOrElse(3600)) +// _ <- Future(Consents.consentProvider.vend.setValidUntil(createdConsent.consentId, validUntil)) map { +// i => connectorEmptyResponse(i, callContext) +// } +// //we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow. +// //this is from callContext +// grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown") +// //this is from json body +// granteeConsumerId = postConsentBodyCommonJson.consumer_id.getOrElse("Unknown") +// +// shouldSkipConsentScaForConsumerIdPair = APIUtil.skipConsentScaForConsumerIdPairs.contains( +// APIUtil.ConsumerIdPair( +// grantorConsumerId, +// granteeConsumerId +// )) +// mappedConsent <- if (shouldSkipConsentScaForConsumerIdPair) { +// Future{ +// MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head +// } +// } else { +// val challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText" +// scaMethod match { +// case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email +// sendEmailNotification(callContext, consentRequestJson, challengeText) +// case v if v == StrongCustomerAuthentication.SMS.toString => +// sendSmsNotification(callContext, consentRequestJson, challengeText) +// case v if v == StrongCustomerAuthentication.IMPLICIT.toString => +// for { +// (consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext) +// status <- consentImplicitSCA.scaMethod match { +// case v if v == StrongCustomerAuthentication.EMAIL => // Send the email +// sendEmailNotification(callContext, consentRequestJson.copy(email = Some(consentImplicitSCA.recipient)), challengeText) +// case v if v == StrongCustomerAuthentication.SMS => +// sendSmsNotification(callContext, consentRequestJson.copy(phone_number = Some(consentImplicitSCA.recipient)), challengeText) +// case _ => Future { +// "Success" +// } +// }} yield { +// status +// } +// case _ => Future { +// "Success" +// } +// } +// Future{createdConsent} +// } +// } yield { +// (ConsentJsonV500( +// mappedConsent.consentId, +// consentJWT, +// mappedConsent.status, +// Some(mappedConsent.consentRequestId), +// if (isVRPConsentRequest) Some( +// ConsentAccountAccessJson( +// bankId.value, +// accountId.value, +// viewId.value, +// Some(HelperInfoJson(List(counterpartyId.value)))) +// ) +// else None +// ), HttpCode.`201`(callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// headAtms, +// implementedInApiVersion, +// nameOf(headAtms), +// "HEAD", +// "/banks/BANK_ID/atms", +// "Head Bank ATMS", +// s"""Head Bank ATMS.""", +// EmptyBody, +// atmsJsonV400, +// List( +// $BankNotFound, +// UnknownError +// ), +// List(apiTagATM) +// ) +// lazy val headAtms : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "atms" :: Nil JsonHead _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, callContext) <- getAtmsIsPublic match { +// case false => authenticatedAccess(cc) +// case true => anonymousAccess(cc) +// } +// } yield { +// ("", HttpCode.`200`(callContext)) +// } +// } +// } +// +// +// +// staticResourceDocs += ResourceDoc( +// createCustomer, +// implementedInApiVersion, +// nameOf(createCustomer), +// "POST", +// "/banks/BANK_ID/customers", +// "Create Customer", +// s""" +// |The Customer resource stores the customer number (which is set by the backend), legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc. +// |Dates need to be in the format 2013-01-21T23:08:00Z +// | +// |If kyc_status is not provided, it defaults to false. +// | +// |Note: If you need to set a specific customer number, use the Update Customer Number endpoint after this call. +// | +// |${userAuthenticationMessage(true)} +// |""", +// postCustomerJsonV500, +// customerJsonV310, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// InvalidJsonFormat, +// CustomerNumberAlreadyExists, +// UserNotFoundById, +// CustomerAlreadyExistsForUser, +// CreateConsumerError, +// UnknownError +// ), +// List(apiTagCustomer, apiTagPerson), +// Some(List(canCreateCustomer,canCreateCustomerAtAnyBank)) +// ) +// lazy val createCustomer : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customers" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerJsonV500 ", 400, cc.callContext) { +// json.extract[PostCustomerJsonV500] +// } +// _ <- Helper.booleanToFuture(failMsg = InvalidJsonContent + s" The field dependants(${postedData.dependants.getOrElse(0)}) not equal the length(${postedData.dob_of_dependants.getOrElse(Nil).length }) of dob_of_dependants array", 400, cc.callContext) { +// postedData.dependants.getOrElse(0) == postedData.dob_of_dependants.getOrElse(Nil).length +// } +// customerNumber = postedData.customer_number.getOrElse(Random.nextInt(Integer.MAX_VALUE).toString) +// +// _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat customer_number can not contain `::::` characters", cc=cc.callContext) { +// !`checkIfContains::::` (customerNumber) +// } +// (_, callContext) <- NewStyle.function.checkCustomerNumberAvailable(bankId, customerNumber, cc.callContext) +// (customer, callContext) <- NewStyle.function.createCustomerC2( +// bankId, +// postedData.legal_name, +// customerNumber, +// postedData.mobile_phone_number, +// postedData.email.getOrElse(""), +// CustomerFaceImage( +// postedData.face_image.map(_.date).getOrElse(null), +// postedData.face_image.map(_.url).getOrElse("") +// ), +// postedData.date_of_birth.getOrElse(null), +// postedData.relationship_status.getOrElse(""), +// postedData.dependants.getOrElse(0), +// postedData.dob_of_dependants.getOrElse(Nil), +// postedData.highest_education_attained.getOrElse(""), +// postedData.employment_status.getOrElse(""), +// postedData.kyc_status.getOrElse(false), +// postedData.last_ok_date.getOrElse(null), +// postedData.credit_rating.map(i => CreditRating(i.rating, i.source)), +// postedData.credit_limit.map(i => CreditLimit(i.currency, i.amount)), +// postedData.title.getOrElse(""), +// postedData.branch_id.getOrElse(""), +// postedData.name_suffix.getOrElse(""), +// "", +// "", +// callContext, +// ) +// } yield { +// (JSONFactory310.createCustomerJson(customer), HttpCode.`201`(callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// getCustomerOverview, +// implementedInApiVersion, +// nameOf(getCustomerOverview), +// "POST", +// "/banks/BANK_ID/customers/customer-number-query/overview", +// "Get Customer Overview", +// s"""Gets the Customer Overview specified by customer_number and bank_code. +// | +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// postCustomerOverviewJsonV500, +// customerOverviewJsonV500, +// List( +// AuthenticatedUserIsRequired, +// UserCustomerLinksNotFoundForUser, +// UnknownError +// ), +// List(apiTagCustomer, apiTagKyc), +// Some(List(canGetCustomerOverview)) +// ) +// +// lazy val getCustomerOverview : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customers" :: "customer-number-query" :: "overview" :: Nil JsonPost json -> req => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 ", 400, cc.callContext) { +// json.extract[PostCustomerOverviewJsonV500] +// } +// (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bankId, cc.callContext) +// (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( +// bankId, +// CustomerId(customer.customerId), +// callContext: Option[CallContext]) +// accountIds <- AccountAttributeX.accountAttributeProvider.vend +// .getAccountIdsByParams(bankId, List("customer_number" -> List(postedData.customer_number)).toMap) +// (accounts: List[BankAccount], callContext) <- NewStyle.function.getBankAccounts(accountIds.toList.flatten.map(i => BankIdAccountId(bankId, AccountId(i))), callContext) +// (accountAttributes, callContext) <- NewStyle.function.getAccountAttributesForAccounts( +// bankId, +// accounts, +// callContext: Option[CallContext]) +// } yield { +// (JSONFactory500.createCustomerWithAttributesJson(customer, customerAttributes, accountAttributes), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getCustomerOverviewFlat, +// implementedInApiVersion, +// nameOf(getCustomerOverviewFlat), +// "POST", +// "/banks/BANK_ID/customers/customer-number-query/overview-flat", +// "Get Customer Overview Flat", +// s"""Gets the Customer Overview Flat specified by customer_number and bank_code. +// | +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// postCustomerOverviewJsonV500, +// customerOverviewFlatJsonV500, +// List( +// AuthenticatedUserIsRequired, +// UserCustomerLinksNotFoundForUser, +// UnknownError +// ), +// List(apiTagCustomer, apiTagKyc), +// Some(List(canGetCustomerOverviewFlat)) +// ) +// +// lazy val getCustomerOverviewFlat : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customers" :: "customer-number-query" :: "overview-flat" :: Nil JsonPost json -> req => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 ", 400, cc.callContext) { +// json.extract[PostCustomerOverviewJsonV500] +// } +// (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bankId, cc.callContext) +// (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( +// bankId, +// CustomerId(customer.customerId), +// callContext: Option[CallContext]) +// accountIds <- AccountAttributeX.accountAttributeProvider.vend +// .getAccountIdsByParams(bankId, List("customer_number" -> List(postedData.customer_number)).toMap) +// (accounts: List[BankAccount], callContext) <- NewStyle.function.getBankAccounts(accountIds.toList.flatten.map(i => BankIdAccountId(bankId, AccountId(i))), callContext) +// (accountAttributes, callContext) <- NewStyle.function.getAccountAttributesForAccounts( +// bankId, +// accounts, +// callContext: Option[CallContext]) +// } yield { +// (JSONFactory500.createCustomerOverviewFlatJson(customer, customerAttributes, accountAttributes), HttpCode.`200`(callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// getMyCustomersAtAnyBank, +// implementedInApiVersion, +// nameOf(getMyCustomersAtAnyBank), +// "GET", +// "/my/customers", +// "Get My Customers", +// """Gets all Customers that are linked to me. +// | +// |Authentication via OAuth is required.""", +// EmptyBody, +// customerJsonV210, +// List( +// $AuthenticatedUserIsRequired, +// UserCustomerLinksNotFoundForUser, +// UnknownError +// ), +// List(apiTagCustomer, apiTagUser)) +// +// lazy val getMyCustomersAtAnyBank : OBPEndpoint = { +// case "my" :: "customers" :: Nil JsonGet _ => { +// cc => { +// implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), callContext) <- SS.user +// (customers, callContext) <- Connector.connector.vend.getCustomersByUserId(u.userId, callContext) map { +// connectorEmptyResponse(_, callContext) +// } +// } yield { +// (JSONFactory210.createCustomersJson(customers), HttpCode.`200`(callContext)) +// } +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getMyCustomersAtBank, +// implementedInApiVersion, +// nameOf(getMyCustomersAtBank), +// "GET", +// "/banks/BANK_ID/my/customers", +// "Get My Customers at Bank", +// s"""Returns a list of Customers at the Bank that are linked to the currently authenticated User. +// | +// | +// |${userAuthenticationMessage(true)}""".stripMargin, +// EmptyBody, +// customerJSONs, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UserCustomerLinksNotFoundForUser, +// UnknownError +// ), +// List(apiTagCustomer) +// ) +// +// lazy val getMyCustomersAtBank : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "my" :: "customers" :: Nil JsonGet _ => { +// cc => { +// implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), callContext) <- SS.user +// (_, callContext) <- NewStyle.function.getBank(bankId, callContext) +// (customers, callContext) <- Connector.connector.vend.getCustomersByUserId(u.userId, callContext) map { +// connectorEmptyResponse(_, callContext) +// } +// } yield { +// // Filter so we only see the ones for the bank in question +// val bankCustomers = customers.filter(_.bankId==bankId.value) +// val json = JSONFactory210.createCustomersJson(bankCustomers) +// (json, HttpCode.`200`(callContext)) +// } +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// getCustomersAtOneBank, +// implementedInApiVersion, +// nameOf(getCustomersAtOneBank), +// "GET", +// "/banks/BANK_ID/customers", +// "Get Customers at Bank", +// s"""Get Customers at Bank. +// | +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// EmptyBody, +// customersJsonV300, +// List( +// AuthenticatedUserIsRequired, +// UserCustomerLinksNotFoundForUser, +// UnknownError +// ), +// List(apiTagCustomer, apiTagUser), +// Some(List(canGetCustomersAtOneBank)) +// ) +// +// lazy val getCustomersAtOneBank : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customers" :: Nil JsonGet _ => { +// cc => { +// implicit val ec = EndpointContext(Some(cc)) +// for { +// (requestParams, callContext) <- extractQueryParams(cc.url, List("limit","offset","sort_direction"), cc.callContext) +// customers <- NewStyle.function.getCustomers(bankId, callContext, requestParams) +// } yield { +// (JSONFactory300.createCustomersJson(customers.sortBy(_.bankId)), HttpCode.`200`(callContext)) +// } +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getCustomersMinimalAtOneBank, +// implementedInApiVersion, +// nameOf(getCustomersMinimalAtOneBank), +// "GET", +// "/banks/BANK_ID/customers-minimal", +// "Get Customers Minimal at Bank", +// s"""Get Customers Minimal at Bank. +// | +// | +// | +// |""", +// EmptyBody, +// customersMinimalJsonV300, +// List( +// UserCustomerLinksNotFoundForUser, +// UnknownError +// ), +// List(apiTagCustomer, apiTagUser), +// Some(List(canGetCustomersMinimalAtOneBank)) +// ) +// lazy val getCustomersMinimalAtOneBank : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customers-minimal" :: Nil JsonGet _ => { +// cc => { +// implicit val ec = EndpointContext(Some(cc)) +// for { +// (requestParams, callContext) <- extractQueryParams(cc.url, List("limit","offset","sort_direction"), cc.callContext) +// customers <- NewStyle.function.getCustomers(bankId, callContext, requestParams) +// } yield { +// (createCustomersMinimalJson(customers.sortBy(_.bankId)), HttpCode.`200`(callContext)) +// } +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// createProduct, +// implementedInApiVersion, +// nameOf(createProduct), +// "PUT", +// "/banks/BANK_ID/products/PRODUCT_CODE", +// "Create Product", +// s"""Create or Update Product for the Bank. +// | +// |The combination of bank_id and product_code is unique. If a Product already exists for the bank_id and product_code, it will be updated. +// | +// |Typical Super Family values / Asset classes are: +// | +// |Debt +// |Equity +// |FX +// |Commodity +// |Derivative +// | +// |$productHiearchyAndCollectionNote +// | +// | +// |${userAuthenticationMessage(true) } +// | +// | +// |""", +// putProductJsonV500, +// productJsonV400.copy(attributes = None, fees = None), +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagProduct), +// Some(List(canCreateProduct, canCreateProductAtAnyBank)) +// ) +// lazy val createProduct: OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "products" :: ProductCode(productCode) :: Nil JsonPut json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), callContext) <- SS.user +// _ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = createProductEntitlementsRequiredText)(bankId.value, u.userId, createProductEntitlements, callContext) +// failMsg = s"$InvalidJsonFormat The Json body should be the $PutProductJsonV500 " +// product <- NewStyle.function.tryons(failMsg, 400, callContext) { +// json.extract[PutProductJsonV500] +// } +// (parentProduct, callContext) <- product.parent_product_code.trim.nonEmpty match { +// case false => +// Future((Empty, callContext)) +// case true => +// NewStyle.function.getProduct(bankId, ProductCode(product.parent_product_code), callContext).map(product => (Full(product._1),product._2)) +// } +// (success, callContext) <- NewStyle.function.createOrUpdateProduct( +// bankId = bankId.value, +// code = productCode.value, +// parentProductCode = parentProduct.map(_.code.value).toOption, +// name = product.name, +// category = null, +// family = null, +// superFamily = null, +// moreInfoUrl = product.more_info_url.getOrElse(""), +// termsAndConditionsUrl = product.terms_and_conditions_url.getOrElse(""), +// details = null, +// description = product.description.getOrElse(""), +// metaLicenceId = product.meta.map(_.license.id).getOrElse(""), +// metaLicenceName = product.meta.map(_.license.name).getOrElse(""), +// callContext +// ) +// } yield { +// (JSONFactory400.createProductJson(success), HttpCode.`201`(callContext)) +// } +// } +// } +// +// +// +// staticResourceDocs += ResourceDoc( +// addCardForBank, +// implementedInApiVersion, +// nameOf(addCardForBank), +// "POST", +// "/management/banks/BANK_ID/cards", +// "Create Card", +// s"""Create Card at bank specified by BANK_ID . +// | +// |${userAuthenticationMessage(true)} +// |""", +// createPhysicalCardJsonV500, +// physicalCardJsonV500, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UserHasMissingRoles, +// AllowedValuesAre, +// UnknownError +// ), +// List(apiTagCard), +// Some(List(canCreateCardsForBank))) +// lazy val addCardForBank: OBPEndpoint = { +// case "management" :: "banks" :: BankId(bankId) :: "cards" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), _,callContext) <- SS.userBank +// +// failMsg = s"$InvalidJsonFormat The Json body should be the $CreatePhysicalCardJsonV500 " +// postJson <- NewStyle.function.tryons(failMsg, 400, callContext) {json.extract[CreatePhysicalCardJsonV500]} +// +// _ <- postJson.allows match { +// case List() => Future {true} +// case _ => Helper.booleanToFuture(AllowedValuesAre + CardAction.availableValues.mkString(", "), cc=callContext)(postJson.allows.forall(a => CardAction.availableValues.contains(a))) +// } +// +// failMsg = AllowedValuesAre + CardReplacementReason.availableValues.mkString(", ") +// cardReplacementReason <- NewStyle.function.tryons(failMsg, 400, callContext) { +// postJson.replacement match { +// case Some(value) => CardReplacementReason.valueOf(value.reason_requested) +// case None => CardReplacementReason.valueOf(CardReplacementReason.FIRST.toString) +// } +// } +// +// _<-Helper.booleanToFuture(s"${maximumLimitExceeded.replace("10000", "10")} Current issue_number is ${postJson.issue_number}", cc=callContext)(postJson.issue_number.length<= 10) +// +// (_, callContext)<- NewStyle.function.getBankAccount(bankId, AccountId(postJson.account_id), callContext) +// +// (_, callContext)<- NewStyle.function.getCustomerByCustomerId(postJson.customer_id, callContext) +// +// replacement = postJson.replacement match { +// case Some(replacement) => +// Some(CardReplacementInfo(requestedDate = replacement.requested_date, cardReplacementReason)) +// case None => None +// } +// collected = postJson.collected match { +// case Some(collected) => Some(CardCollectionInfo(collected)) +// case None => None +// } +// posted = postJson.posted match { +// case Some(posted) => Option(CardPostedInfo(posted)) +// case None => None +// } +// +// cvv = ThreadLocalRandom.current().nextLong(100, 999) +// +// (card, callContext) <- NewStyle.function.createPhysicalCard( +// bankCardNumber=postJson.card_number, +// nameOnCard=postJson.name_on_card, +// cardType = postJson.card_type, +// issueNumber=postJson.issue_number, +// serialNumber=postJson.serial_number, +// validFrom=postJson.valid_from_date, +// expires=postJson.expires_date, +// enabled=postJson.enabled, +// cancelled=false, +// onHotList=false, +// technology=postJson.technology, +// networks= postJson.networks, +// allows= postJson.allows, +// accountId= postJson.account_id, +// bankId=bankId.value, +// replacement = replacement, +// pinResets= postJson.pin_reset.map(e => PinResetInfo(e.requested_date, PinResetReason.valueOf(e.reason_requested.toUpperCase))), +// collected = collected, +// posted = posted, +// customerId = postJson.customer_id, +// cvv = cvv.toString, +// brand = postJson.brand, +// callContext +// ) +// } yield { +// //NOTE: OBP do not store the 3 digits cvv, only the hash, so we copy it here. +// (createPhysicalCardJson(card, u).copy(cvv=cvv.toString), HttpCode.`201`(callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// getViewsForBankAccount, +// implementedInApiVersion, +// nameOf(getViewsForBankAccount), +// "GET", +// "/banks/BANK_ID/accounts/ACCOUNT_ID/views", +// "Get Views for Account", +// s"""#Views +// | +// | +// |Views in Open Bank Project provide a mechanism for fine grained access control and delegation to Accounts and Transactions. Account holders use the 'owner' view by default. Delegated access is made through other views for example 'accountants', 'share-holders' or 'tagging-application'. Views can be created via the API and each view has a list of entitlements. +// | +// |Views on accounts and transactions filter the underlying data to redact certain fields for certain users. For instance the balance on an account may be hidden from the public. The way to know what is possible on a view is determined in the following JSON. +// | +// |**Data:** When a view moderates a set of data, some fields my contain the value `null` rather than the original value. This indicates either that the user is not allowed to see the original data or the field is empty. +// | +// |There is currently one exception to this rule; the 'holder' field in the JSON contains always a value which is either an alias or the real name - indicated by the 'is_alias' field. +// | +// |**Action:** When a user performs an action like trying to post a comment (with POST API call), if he is not allowed, the body response will contain an error message. +// | +// |**Metadata:** +// |Transaction metadata (like images, tags, comments, etc.) will appears *ONLY* on the view where they have been created e.g. comments posted to the public view only appear on the public view. +// | +// |The other account metadata fields (like image_URL, more_info, etc.) are unique through all the views. Example, if a user edits the 'more_info' field in the 'team' view, then the view 'authorities' will show the new value (if it is allowed to do it). +// | +// |# All +// |*Optional* +// | +// |Returns the list of the views created for account ACCOUNT_ID at BANK_ID. +// | +// |${userAuthenticationMessage(true)} and the user needs to have access to the owner view.""", +// EmptyBody, +// viewsJsonV500, +// List( +// $AuthenticatedUserIsRequired, +// $BankAccountNotFound, +// UnknownError +// ), +// List(apiTagView, apiTagAccount)) +// +// lazy val getViewsForBankAccount : OBPEndpoint = { +// //get the available views on an bank account +// case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: Nil JsonGet req => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// val res = +// for { +// (Full(u), callContext) <- SS.user +// permission <- NewStyle.function.permission(bankId, accountId, u, callContext) +// anyViewContainsCanSeeAvailableViewsForBankAccountPermission = permission.views.map(_.allowed_actions.exists(_ == CAN_SEE_AVAILABLE_VIEWS_FOR_BANK_ACCOUNT)).find(_.==(true)).getOrElse(false) +// _ <- Helper.booleanToFuture( +// s"${ErrorMessages.ViewDoesNotPermitAccess} You need the `${(CAN_SEE_AVAILABLE_VIEWS_FOR_BANK_ACCOUNT)}` permission on any your views", +// cc = callContext +// ) { +// anyViewContainsCanSeeAvailableViewsForBankAccountPermission +// } +// } yield { +// for { +// views <- Full(Views.views.vend.availableViewsForAccount(BankIdAccountId(bankId, accountId))) +// } yield { +// (createViewsJsonV500(views), HttpCode.`200`(cc.callContext)) +// } +// } +// res map { fullBoxOrException(_) } map { unboxFull(_) } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// deleteSystemView, +// implementedInApiVersion, +// "deleteSystemView", +// "DELETE", +// "/system-views/VIEW_ID", +// "Delete System View", +// "Deletes the system view specified by VIEW_ID", +// EmptyBody, +// EmptyBody, +// List( +// AuthenticatedUserIsRequired, +// BankAccountNotFound, +// UnknownError, +// "user does not have owner access" +// ), +// List(apiTagSystemView), +// Some(List(canDeleteSystemView)) +// ) +// +// lazy val deleteSystemView: OBPEndpoint = { +// case "system-views" :: viewId :: Nil JsonDelete req => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// _ <- ViewNewStyle.systemView(ViewId(viewId), cc.callContext) +// view <- ViewNewStyle.deleteSystemView(ViewId(viewId), cc.callContext) +// } yield { +// (Full(view), HttpCode.`200`(cc.callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getMetricsAtBank, +// implementedInApiVersion, +// nameOf(getMetricsAtBank), +// "GET", +// "/management/metrics/banks/BANK_ID", +// "Get Metrics at Bank", +// s"""Get the all metrics at the Bank specified by BANK_ID +// | +// |require CanReadMetrics role +// | +// |Filters Part 1.*filtering* (no wilde cards etc.) parameters to GET /management/metrics +// | +// |Should be able to filter on the following metrics fields +// | +// |eg: /management/metrics?from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString&limit=50&offset=2 +// | +// |1 from_date (defaults to one week before current date): eg:from_date=$DateWithMsExampleString +// | +// |2 to_date (defaults to current date) eg:to_date=$DateWithMsExampleString +// | +// |3 limit (for pagination: defaults to 50) eg:limit=200 +// | +// |4 offset (for pagination: zero index, defaults to 0) eg: offset=10 +// | +// |5 sort_by (defaults to date field) eg: sort_by=date +// | possible values: +// | "url", +// | "date", +// | "username" (or "user_name" for backward compatibility), +// | "app_name", +// | "developer_email", +// | "implemented_by_partial_function", +// | "implemented_in_version", +// | "consumer_id", +// | "verb" +// | +// |6 direction (defaults to date desc) eg: direction=desc +// | +// |eg: /management/metrics?from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString&limit=10000&offset=0&anon=false&app_name=TeatApp&implemented_in_version=v2.1.0&verb=POST&user_id=c7b6cb47-cb96-4441-8801-35b57456753a&username=susan.uk.29@example.com&consumer_id=78 +// | +// |Other filters: +// | +// |7 consumer_id (if null ignore) +// | +// |8 user_id (if null ignore) +// | +// |9 anon (if null ignore) only support two value : true (return where user_id is null.) or false (return where user_id is not null.) +// | +// |10 url (if null ignore), note: can not contain '&'. +// | +// |11 app_name (if null ignore) +// | +// |12 implemented_by_partial_function (if null ignore), +// | +// |13 implemented_in_version (if null ignore) +// | +// |14 verb (if null ignore) +// | +// |15 correlation_id (if null ignore) +// | +// |16 duration (if null ignore) non digit chars will be silently omitted +// | +// """.stripMargin, +// EmptyBody, +// metricsJson, +// List( +// $AuthenticatedUserIsRequired, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagMetric, apiTagApi), +// Some(List(canGetMetricsAtOneBank))) +// +// lazy val getMetricsAtBank : OBPEndpoint = { +// case "management" :: "metrics" :: "banks" :: bankId :: Nil JsonGet _ => { +// cc => { +// implicit val ec = EndpointContext(Some(cc)) +// for { +// httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) +// (obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext) +// metrics <- Future(APIMetrics.apiMetrics.vend.getAllMetrics(obpQueryParams ::: List(OBPBankId(bankId)))) +// } yield { +// (JSONFactory210.createMetricsJson(metrics), HttpCode.`200`(callContext)) +// } +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getSystemView, +// implementedInApiVersion, +// "getSystemView", +// "GET", +// "/system-views/VIEW_ID", +// "Get System View", +// s"""Get System View +// | +// |${userAuthenticationMessage(true)} +// | +// """.stripMargin, +// EmptyBody, +// viewJsonV500, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UnknownError +// ), +// List(apiTagSystemView), +// Some(List(canGetSystemView)) +// ) +// +// lazy val getSystemView: OBPEndpoint = { +// case "system-views" :: viewId :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// view <- ViewNewStyle.systemView(ViewId(viewId), cc.callContext) +// } yield { +// (createViewJsonV500(view), HttpCode.`200`(cc.callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getSystemViewsIds, +// implementedInApiVersion, +// nameOf(getSystemViewsIds), +// "GET", +// "/system-views-ids", +// "Get Ids of System Views", +// s"""Get Ids of System Views +// | +// |${userAuthenticationMessage(true)} +// | +// """.stripMargin, +// EmptyBody, +// viewIdsJsonV500, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UnknownError +// ), +// List(apiTagSystemView), +// Some(List(canGetSystemView)) +// ) +// +// lazy val getSystemViewsIds: OBPEndpoint = { +// case "system-views-ids" :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// views <- ViewNewStyle.systemViews() +// } yield { +// (createViewsIdsJsonV500(views), HttpCode.`200`(cc.callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// createSystemView, +// implementedInApiVersion, +// nameOf(createSystemView), +// "POST", +// "/system-views", +// "Create System View", +// s"""Create a system view +// | +// | ${userAuthenticationMessage(true)} and the user needs to have access to the $canCreateSystemView entitlement. +// | +// | The 'allowed_actions' field is a list containing the names of the actions allowed through this view. +// | All the actions contained in the list will be set to `true` on the view creation, the rest will be set to `false`. +// | +// | The 'alias' field in the JSON can take one of three values: +// | +// | * _public_: to use the public alias if there is one specified for the other account. +// | * _private_: to use the private alias if there is one specified for the other account. +// | * _''(empty string)_: to use no alias; the view shows the real name of the other account. +// | +// | The 'hide_metadata_if_alias_used' field in the JSON can take boolean values. If it is set to `true` and there is an alias on the other account then the other accounts' metadata (like more_info, url, image_url, open_corporates_url, etc.) will be hidden. Otherwise the metadata will be shown. +// | +// | The 'metadata_view' field determines where metadata (comments, tags, images, where tags) for transactions are stored and retrieved. If set to another view's ID (e.g. 'owner'), metadata added through this view will be shared with all other views that also use the same metadata_view value. If left empty, metadata is stored under this view's own ID and is not shared with other views. +// | +// | System views cannot be public. In case you try to set it you will get the error $SystemViewCannotBePublicError +// | """, +// createSystemViewJsonV500, +// viewJsonV500, +// List( +// $AuthenticatedUserIsRequired, +// InvalidJsonFormat, +// UnknownError +// ), +// List(apiTagSystemView), +// Some(List(canCreateSystemView)) +// ) +// +// lazy val createSystemView : OBPEndpoint = { +// //creates a system view +// case "system-views" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// createViewJson <- NewStyle.function.tryons(failMsg = s"$InvalidJsonFormat The Json body should be the $CreateViewJson ", 400, cc.callContext) { +// json.extract[CreateViewJsonV500] +// } +// _ <- Helper.booleanToFuture(SystemViewCannotBePublicError, failCode=400, cc=cc.callContext) { +// createViewJson.is_public == false +// } +// // custom views are started with `_`,eg _ life, _ work, and System views can not, eg: owner. +// _ <- Helper.booleanToFuture(failMsg = InvalidSystemViewFormat +s"Current view_name (${createViewJson.name})", cc = cc.callContext) { +// isValidSystemViewName(createViewJson.name) +// } +// view <- ViewNewStyle.createSystemView(createViewJson.toCreateViewJson, cc.callContext) +// } yield { +// (createViewJsonV500(view), HttpCode.`201`(cc.callContext)) +// } +// } +// } +// +// +// staticResourceDocs += ResourceDoc( +// updateSystemView, +// implementedInApiVersion, +// nameOf(updateSystemView), +// "PUT", +// "/system-views/VIEW_ID", +// "Update System View", +// s"""Update an existing view on a bank account +// | +// |${userAuthenticationMessage(true)} and the user needs to have access to the owner view. +// | +// |The json sent is the same as during view creation (above), with one difference: the 'name' field +// |of a view is not editable (it is only set when a view is created)""", +// updateSystemViewJson500, +// viewJsonV500, +// List( +// InvalidJsonFormat, +// $AuthenticatedUserIsRequired, +// BankAccountNotFound, +// UnknownError +// ), +// List(apiTagSystemView), +// Some(List(canUpdateSystemView)) +// ) +// +// lazy val updateSystemView : OBPEndpoint = { +// //updates a view on a bank account +// case "system-views" :: viewId :: Nil JsonPut json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// updateJson <- Future { tryo{json.extract[UpdateViewJsonV500]} } map { +// val msg = s"$InvalidJsonFormat The Json body should be the $UpdateViewJSON " +// x => unboxFullOrFail(x, cc.callContext, msg) +// } +// _ <- Helper.booleanToFuture(SystemViewCannotBePublicError, failCode=400, cc=cc.callContext) { +// updateJson.is_public == false +// } +// _ <- ViewNewStyle.systemView(ViewId(viewId), cc.callContext) +// updatedView <- ViewNewStyle.updateSystemView(ViewId(viewId), updateJson.toUpdateViewJson, cc.callContext) +// } yield { +// (createViewJsonV500(updatedView), HttpCode.`200`(cc.callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// createCustomerAccountLink, +// implementedInApiVersion, +// nameOf(createCustomerAccountLink), +// "POST", +// "/banks/BANK_ID/customer-account-links", +// "Create Customer Account Link", +// s"""Link a Customer to a Account +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// createCustomerAccountLinkJson, +// customerAccountLinkJson, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// BankAccountNotFound, +// InvalidJsonFormat, +// CustomerNotFoundByCustomerId, +// UserHasMissingRoles, +// AccountAlreadyExistsForCustomer, +// CreateCustomerAccountLinkError, +// UnknownError +// ), +// List(apiTagCustomer, apiTagAccount), +// Some(List(canCreateCustomerAccountLink))) +// lazy val createCustomerAccountLink : OBPEndpoint = { +// case "banks" :: BankId(bankId):: "customer-account-links" :: Nil JsonPost json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, _,callContext) <- SS.userBank +// +// postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $CreateCustomerAccountLinkJson ", 400, callContext) { +// json.extract[CreateCustomerAccountLinkJson] +// } +// (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(postedData.customer_id, callContext) +// _ <- booleanToFuture(s"Bank of the customer specified by the CUSTOMER_ID(${customer.bankId}) has to matches BANK_ID(${bankId.value}) in URL", 400, callContext) { +// customer.bankId == bankId.value +// } +// (_, callContext) <- NewStyle.function.getBankAccount(bankId, AccountId(postedData.account_id), callContext) +// _ <- booleanToFuture("Field customer_id is not defined in the posted json!", 400, callContext) { +// postedData.customer_id.nonEmpty +// } +// (customerAccountLinkExists, callContext) <- Connector.connector.vend.getCustomerAccountLink(postedData.customer_id, postedData.account_id, callContext) +// _ <- booleanToFuture(AccountAlreadyExistsForCustomer, 400, callContext) { +// customerAccountLinkExists.isEmpty +// } +// (customerAccountLink, callContext) <- NewStyle.function.createCustomerAccountLink(postedData.customer_id, postedData.bank_id, postedData.account_id, postedData.relationship_type, callContext) +// } yield { +// (JSONFactory500.createCustomerAccountLinkJson(customerAccountLink), HttpCode.`201`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getCustomerAccountLinksByCustomerId, +// implementedInApiVersion, +// nameOf(getCustomerAccountLinksByCustomerId), +// "GET", +// "/banks/BANK_ID/customers/CUSTOMER_ID/customer-account-links", +// "Get Customer Account Links by CUSTOMER_ID", +// s""" Get Customer Account Links by CUSTOMER_ID +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// EmptyBody, +// customerAccountLinksJson, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// CustomerNotFoundByCustomerId, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagCustomer), +// Some(List(canGetCustomerAccountLinks))) +// lazy val getCustomerAccountLinksByCustomerId : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customers" :: customerId :: "customer-account-links" :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, cc.callContext) +// _ <- booleanToFuture(s"Bank of the customer specified by the CUSTOMER_ID(${customer.bankId}) has to matches BANK_ID(${bankId.value}) in URL", 400, callContext) { +// customer.bankId == bankId.value +// } +// (customerAccountLinks, callContext) <- NewStyle.function.getCustomerAccountLinksByCustomerId(customerId, callContext) +// } yield { +// (JSONFactory500.createCustomerAccountLinksJon(customerAccountLinks), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getCustomerAccountLinksByBankIdAccountId, +// implementedInApiVersion, +// nameOf(getCustomerAccountLinksByBankIdAccountId), +// "GET", +// "/banks/BANK_ID/accounts/ACCOUNT_ID/customer-account-links", +// "Get Customer Account Links by ACCOUNT_ID", +// s""" Get Customer Account Links by ACCOUNT_ID +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// EmptyBody, +// customerAccountLinksJson, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// BankAccountNotFound, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagCustomer), +// Some(List(canGetCustomerAccountLinks))) +// lazy val getCustomerAccountLinksByBankIdAccountId : OBPEndpoint = { +// case "banks" :: bankId :: "accounts" :: accountId :: "customer-account-links" :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, _,callContext) <- SS.userBank +// (customerAccountLinks, callContext) <- NewStyle.function.getCustomerAccountLinksByBankIdAccountId(bankId, accountId, callContext) +// } yield { +// (JSONFactory500.createCustomerAccountLinksJon(customerAccountLinks), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// getCustomerAccountLinkById, +// implementedInApiVersion, +// nameOf(getCustomerAccountLinkById), +// "GET", +// "/banks/BANK_ID/customer-account-links/CUSTOMER_ACCOUNT_LINK_ID", +// "Get Customer Account Link by Id", +// s""" Get Customer Account Link by CUSTOMER_ACCOUNT_LINK_ID +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// EmptyBody, +// customerAccountLinkJson, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagCustomer), +// Some(List(canGetCustomerAccountLink))) +// lazy val getCustomerAccountLinkById : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customer-account-links" :: customerAccountLinkId :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, _,callContext) <- SS.userBank +// (customerAccountLink, callContext) <- NewStyle.function.getCustomerAccountLinkById(customerAccountLinkId, callContext) +// } yield { +// (JSONFactory500.createCustomerAccountLinkJson(customerAccountLink), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// updateCustomerAccountLinkById, +// implementedInApiVersion, +// nameOf(updateCustomerAccountLinkById), +// "PUT", +// "/banks/BANK_ID/customer-account-links/CUSTOMER_ACCOUNT_LINK_ID", +// "Update Customer Account Link by Id", +// s""" Update Customer Account Link by CUSTOMER_ACCOUNT_LINK_ID +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// updateCustomerAccountLinkJson, +// customerAccountLinkJson, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagCustomer), +// Some(List(canUpdateCustomerAccountLink))) +// lazy val updateCustomerAccountLinkById : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customer-account-links" :: customerAccountLinkId :: Nil JsonPut json -> _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), _,callContext) <- SS.userBank +// postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $UpdateCustomerAccountLinkJson ", 400, callContext) { +// json.extract[UpdateCustomerAccountLinkJson] +// } +// (_, callContext) <- NewStyle.function.getCustomerAccountLinkById(customerAccountLinkId, callContext) +// +// (customerAccountLink, callContext) <- NewStyle.function.updateCustomerAccountLinkById(customerAccountLinkId, postedData.relationship_type, callContext) +// } yield { +// (JSONFactory500.createCustomerAccountLinkJson(customerAccountLink), HttpCode.`200`(callContext)) +// } +// } +// } +// +// staticResourceDocs += ResourceDoc( +// deleteCustomerAccountLinkById, +// implementedInApiVersion, +// nameOf(deleteCustomerAccountLinkById), +// "DELETE", +// "/banks/BANK_ID/customer-account-links/CUSTOMER_ACCOUNT_LINK_ID", +// "Delete Customer Account Link", +// s""" Delete Customer Account Link by CUSTOMER_ACCOUNT_LINK_ID +// | +// |${userAuthenticationMessage(true)} +// | +// |""", +// EmptyBody, +// EmptyBody, +// List( +// $AuthenticatedUserIsRequired, +// $BankNotFound, +// UserHasMissingRoles, +// UnknownError +// ), +// List(apiTagCustomer), +// Some(List(canDeleteCustomerAccountLink))) +// lazy val deleteCustomerAccountLinkById : OBPEndpoint = { +// case "banks" :: BankId(bankId) :: "customer-account-links" :: customerAccountLinkId :: Nil JsonDelete _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (Full(u), _,callContext) <- SS.userBank +// (_, callContext) <- NewStyle.function.getCustomerAccountLinkById(customerAccountLinkId, callContext) +// (deleted, callContext) <- NewStyle.function.deleteCustomerAccountLinkById(customerAccountLinkId, callContext) +// } yield { +// (Full(deleted), HttpCode.`204`(callContext)) +// } +// } +// } +// +// resourceDocs += ResourceDoc( +// getAdapterInfo, +// implementedInApiVersion, +// nameOf(getAdapterInfo), +// "GET", +// "/adapter", +// "Get Adapter Info", +// s"""Get basic information about the Adapter. +// | +// |${userAuthenticationMessage(true)} +// | +// """.stripMargin, +// EmptyBody, +// adapterInfoJsonV500, +// List($AuthenticatedUserIsRequired, UserHasMissingRoles, UnknownError), +// List(apiTagApi), +// Some(List(canGetAdapterInfo)) +// ) +// lazy val getAdapterInfo: OBPEndpoint = { +// case "adapter" :: Nil JsonGet _ => { +// cc => implicit val ec = EndpointContext(Some(cc)) +// for { +// (_, callContext) <- SS.user +// (adapterInfo,_) <- NewStyle.function.getAdapterInfo(callContext) +// } yield { +// (JSONFactory500.createAdapterInfoJson(adapterInfo,cc.startTime.getOrElse(Helpers.now).getTime), HttpCode.`200`(callContext)) +// } +// } +// } +// } +//} +// +//object APIMethods500 extends RestHelper with APIMethods500 { +// lazy val newStyleEndpoints: List[(String, String)] = Implementations5_0_0.resourceDocs.map { +// rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString()) +// }.toList +//} +// From 460f9c81a0b025a8ce6a8a96c385ba8860f94374 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 08:59:15 +0200 Subject: [PATCH 4/8] v5.0.0 retirement: update FrozenClassUtil snapshot for 3 added endpoints Http4s500 declares getBanks, getProduct, getProducts with implementedInApiVersion=v5.0.0. These were served by Http4s500 before but absent from the old APIMethods500 resourceDocs snapshot. Regenerate with FrozenClassUtil to record the correct stable API set. --- .../src/test/resources/frozen_type_meta_data | Bin 136480 -> 136495 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/obp-api/src/test/resources/frozen_type_meta_data b/obp-api/src/test/resources/frozen_type_meta_data index f120871b013355adb2b3e213109355026d1a0c64..6f1ec287687c09aeb9460ad9187490dba8a4053a 100644 GIT binary patch delta 1222 zcmYjQU2Icj82;Yvv|WGJ^|WidZtYyxuIm`IV~`9CgLE4h+-5em;SGkIcHc^Ed(L*w zR~W%88Tb=p49@q08zT}mnEAu-To^?!NDMJCCesiTFF+-l5KOqx3x5)g-*-B}oA-R@ zeV*t2{k}KWo35-kZM*=^otXhf8nyf)?(l^1GECrt4P_DuFcm9j1l+49|KoB zkIQw}sHPgVD%Bb3##K+7Jfo=dd2M-CQ5lhRO=I}G=ZPpIAugZQ71dA*tfZ+&=u(Xu z%wQxcWJsIW_A=rr=u}~Jg!2j~xQ41qzCy=JhWPxKiEF%d-e}EW8qbY3?|2JW^ArzA z829h;Z#S063h|th2qD|Vu>}VjBB=Ox2N`MR^qJEJqt&A&dP<-{yy)-6A8qaUm4C?D z#PJbDEgRSnIM&BVfd9`KRL|Ae37&@0Wl_Fwo>nNM4+G(}qgQrqHNxeLq`44Re+}SE zfeF0q=)>EAG~RI}(cd~3;1Ozz^StT*MX0pqdw6~I7+VVv^(7xTbA^IdQyJcBeH8z3 z_u#R{I6R3fjc0B3E}Nz2t&}~QglC#4RZFa-77wcnnt%d$PimL=L?ZaL zJ%};e=azOi{%L#N$%wt6sSBkdzGfe{{QK~4`}U*|4m0BCz2z#EgDNYr3awI=StH*s z;0T^~v?au8lo8pQ=KO?KEU7b0VKs|)P@soUYI@5GJ&xC!ehE$SHg$SZD_bk)Aq6Rc zo6fLBO5z{R9+);eU0q-~`f$>H)jG}MoOc&|fG>KZkj29dF?`?K4ME)WeqrH5_-4yl zh+@iDh8Vu;dl@%4CnfKJI3Aay(2dW?WA>ED*NZpgy&guK^)2LB6(xVxHJBO93<%tX zuRz%GUHyXLeHe)dZ&sKOg$W9CT9^a4+K|nO)0cc+TQ^s1kLrtx&MZ#1fO+3OKBdu` zuJfi3EsI?V|FkeSG1HjAOz^&Rm61+d%LK5Tb>sffrxL&VaqLQjvFi~J#>2sV46GI zlfY+pv||mP#=9MHD4VY62*AhYWK0G7HBr@&dA;*FfX~dwy2b&*CdS_Z`vHNpV=l1| zuH#B#8g84Li3-58=5Wsv!1v~r)KdUIU~~E$oHy6fX@DvGr0*w4;>B%cNTIubkE557 kn0PlzI%+QTuLImPrX`8b43V}VP3>s6Rvj8;!*RSWN@AzZ zO}8O!51U%Vq@qAk4BE*Emx~RNxW9coGr{z(xh3sO7&sYDB{}@1s{niRjhL(X3e0wRZ%T^k&?i6p`{wyIZB%A zKPN3SU(=XiXildiHOtZs`s{@hO!5Dctfp}xX06_mrDY682T=2N!7g0%UTJ17SogLR z27F8VDDgjLs+5Ra{hY29bEa9Gu=2Xa$Yw@PU93{QJX6rm<2_%jrQSDU8r51^xA1{4 z0V4k9`vLpi3B2F5>YgW;iG-$q41aRR@k@Wd#H{D_l1}v-bl=2yftNaoN$7QIHP9o0yf7(2A#}QDK!!+{8Q5 zA*UdPze@@luCC_$J1*|QPh9(*f%o%rk&ehdI>Lia&gWj zIG7xgl%(ok24)2bJH72r>C!uyD7fCGq#d(8627eX@mTCLSB8=A{N6>l_tR%dKn+aE#X^A5_9^E*&M`izIDO#8YNNoU&GBz1RD}Z z;I{pAq8nK7dfSTUkW)08vrT0^ND`|zQQ)efW@oVl#j0|aQoMP0b=8ULI3~& From f35385929401e22d732fc217ddd134aebef39711 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 18:07:25 +0200 Subject: [PATCH 5/8] fix(test): wrap APIUtilHeavyTest versionIsAllowed assertions inside scenario block setPropsValues calls in a feature(...) block body run at class-init time, not inside any scenario. PropsReset.afterEach is never called between class init and the first scenario, so api_enabled_versions=[OBPv4.0.0] leaked into global Props during parallel test execution, causing all v5.0.0/v5.1.0/v6.0.0 endpoints to return 404. --- .../scala/code/util/APIUtilHeavyTest.scala | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala b/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala index 09cdcd0b84..c6c68260da 100644 --- a/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala +++ b/obp-api/src/test/scala/code/util/APIUtilHeavyTest.scala @@ -44,59 +44,61 @@ class APIUtilHeavyTest extends V400ServerSetup with PropsReset { val bgVersion = ConstantsBG.berlinGroupVersion1.apiShortVersion feature("test APIUtil.versionIsAllowed method") { - //This mean, we are only disabled the v4.0.0, all other versions should be enabled - setPropsValues( - "api_disabled_versions" -> "[v4.0.0]", - "api_enabled_versions" -> "[]" - ) - APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) - val allEnabledVersions= ApiVersionUtils.versions.filterNot(_ == ApiVersion.v4_0_0).map(APIUtil.versionIsAllowed) - allEnabledVersions.contains(false) should be (false) - - setPropsValues( - "api_disabled_versions" -> "[OBPv4.0.0]", - "api_enabled_versions" -> "[]" - ) - APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) - val allEnabledVersions2: List[Boolean] = ApiVersionUtils.versions.filterNot(_ == ApiVersion.v4_0_0).map(APIUtil.versionIsAllowed) - allEnabledVersions2.contains(false) should be (false) - - setPropsValues( - "api_disabled_versions" -> "[OBPv4.0.0,v3.1.0,v3.0.0,UKv3.1,UKv2.0]", - "api_enabled_versions" -> "[]" - ) - APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) - APIUtil.versionIsAllowed(ApiVersion.v3_1_0) should be(false) - APIUtil.versionIsAllowed(ApiVersion.v3_0_0) should be(false) - APIUtil.versionIsAllowed(ApiVersionUtils.versions.find(_.fullyQualifiedVersion=="UKv3.1").head) should be(false) - APIUtil.versionIsAllowed(ApiVersionUtils.versions.find(_.fullyQualifiedVersion=="UKv2.0").head) should be(false) - val allEnabledVersions3: List[Boolean] = ApiVersionUtils.versions - .filterNot(_.fullyQualifiedVersion == ApiVersion.v4_0_0.fullyQualifiedVersion) - .filterNot(_.fullyQualifiedVersion == ApiVersion.v3_1_0.fullyQualifiedVersion) - .filterNot(_.fullyQualifiedVersion == ApiVersion.v3_0_0.fullyQualifiedVersion) - .filterNot(_.fullyQualifiedVersion == "UKv3.1") - .filterNot(_.fullyQualifiedVersion == "UKv2.0") - .map(APIUtil.versionIsAllowed) - allEnabledVersions3.contains(false) should be (false) - - - When("we set OBPv4.0.0 both in enabled and disabled props, it should be disabled") - setPropsValues( - "api_disabled_versions" -> "[OBPv4.0.0]", - "api_enabled_versions" -> "[OBPv4.0.0]" - ) - APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) - APIUtil.versionIsAllowed(ApiVersion.v3_1_0) should be(false) - - - When("we set OBPv4.0.0 both in enabled props, it will only enable one version, all other version will be disabled ") - setPropsValues( - "api_disabled_versions" -> "[]", - "api_enabled_versions" -> "[OBPv4.0.0]" - ) - APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(true) - APIUtil.versionIsAllowed(ApiVersion.v3_1_0) should be(false) - APIUtil.versionIsAllowed(ApiVersion.v3_0_0) should be(false) + scenario("Test versionIsAllowed with various disabled/enabled version combinations") { + //This mean, we are only disabled the v4.0.0, all other versions should be enabled + setPropsValues( + "api_disabled_versions" -> "[v4.0.0]", + "api_enabled_versions" -> "[]" + ) + APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) + val allEnabledVersions= ApiVersionUtils.versions.filterNot(_ == ApiVersion.v4_0_0).map(APIUtil.versionIsAllowed) + allEnabledVersions.contains(false) should be (false) + + setPropsValues( + "api_disabled_versions" -> "[OBPv4.0.0]", + "api_enabled_versions" -> "[]" + ) + APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) + val allEnabledVersions2: List[Boolean] = ApiVersionUtils.versions.filterNot(_ == ApiVersion.v4_0_0).map(APIUtil.versionIsAllowed) + allEnabledVersions2.contains(false) should be (false) + + setPropsValues( + "api_disabled_versions" -> "[OBPv4.0.0,v3.1.0,v3.0.0,UKv3.1,UKv2.0]", + "api_enabled_versions" -> "[]" + ) + APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) + APIUtil.versionIsAllowed(ApiVersion.v3_1_0) should be(false) + APIUtil.versionIsAllowed(ApiVersion.v3_0_0) should be(false) + APIUtil.versionIsAllowed(ApiVersionUtils.versions.find(_.fullyQualifiedVersion=="UKv3.1").head) should be(false) + APIUtil.versionIsAllowed(ApiVersionUtils.versions.find(_.fullyQualifiedVersion=="UKv2.0").head) should be(false) + val allEnabledVersions3: List[Boolean] = ApiVersionUtils.versions + .filterNot(_.fullyQualifiedVersion == ApiVersion.v4_0_0.fullyQualifiedVersion) + .filterNot(_.fullyQualifiedVersion == ApiVersion.v3_1_0.fullyQualifiedVersion) + .filterNot(_.fullyQualifiedVersion == ApiVersion.v3_0_0.fullyQualifiedVersion) + .filterNot(_.fullyQualifiedVersion == "UKv3.1") + .filterNot(_.fullyQualifiedVersion == "UKv2.0") + .map(APIUtil.versionIsAllowed) + allEnabledVersions3.contains(false) should be (false) + + + When("we set OBPv4.0.0 both in enabled and disabled props, it should be disabled") + setPropsValues( + "api_disabled_versions" -> "[OBPv4.0.0]", + "api_enabled_versions" -> "[OBPv4.0.0]" + ) + APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(false) + APIUtil.versionIsAllowed(ApiVersion.v3_1_0) should be(false) + + + When("we set OBPv4.0.0 both in enabled props, it will only enable one version, all other version will be disabled ") + setPropsValues( + "api_disabled_versions" -> "[]", + "api_enabled_versions" -> "[OBPv4.0.0]" + ) + APIUtil.versionIsAllowed(ApiVersion.v4_0_0) should be(true) + APIUtil.versionIsAllowed(ApiVersion.v3_1_0) should be(false) + APIUtil.versionIsAllowed(ApiVersion.v3_0_0) should be(false) + } } From 0cbd2ac94da780c2049dda6cf4092e96cad78b24 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 18:07:43 +0200 Subject: [PATCH 6/8] fix(test): add PropsReset to GetScannedApiVersionsTest Scenarios set api_disabled_versions and api_enabled_versions without any cleanup, leaving contaminated Props for subsequent parallel tests. --- .../test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala index a87c0e36f0..11fe1c4d64 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/GetScannedApiVersionsTest.scala @@ -29,13 +29,14 @@ import code.api.util.APIUtil import code.api.util.ApiRole._ import code.api.v4_0_0.APIMethods400.Implementations4_0_0 import code.entitlement.Entitlement +import code.setup.PropsReset import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ListResult import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import org.scalatest.Tag import scala.collection.JavaConverters._ -class GetScannedApiVersionsTest extends V400ServerSetup { +class GetScannedApiVersionsTest extends V400ServerSetup with PropsReset { /** * Test tags From 0200c0825e23fc8dbf7389920c895ef436c39ef4 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 18:08:02 +0200 Subject: [PATCH 7/8] fix(test): guard randomBankId against empty banks list in V500ServerSetup nextInt(0) throws IllegalArgumentException when the list is empty. --- obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala b/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala index 5b770b3b98..97333ebef1 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala @@ -29,6 +29,7 @@ trait V500ServerSetup extends ServerSetupWithTestData with DefaultUsers { makeGetRequest(request) } val banksJson = getBanksInfo.body.extract[BanksJson400] + if (banksJson.banks.isEmpty) return "DEFAULT_BANK_ID_NOT_SET_Test" val randomPosition = nextInt(banksJson.banks.size) val bank = banksJson.banks(randomPosition) bank.id From 4418f0b31d53ac7d5c7f40886cab0a8c7bb7b910 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 19 May 2026 18:08:23 +0200 Subject: [PATCH 8/8] fix(test): guard randomPrivateAccount/randomPrivateAccountViaEndpoint against empty list in V510ServerSetup nextInt(0) throws IllegalArgumentException when the accounts list is empty. --- obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala index a4973ab567..4a88bca733 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala @@ -58,6 +58,7 @@ trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers { def randomPrivateAccount(bankId: String): AccountJSON = { val accountsJson = getPrivateAccounts(bankId, user1).body.extract[AccountsJSON].accounts + if (accountsJson.isEmpty) throw new IllegalStateException(s"No private accounts found for bank $bankId") val randomPosition = nextInt(accountsJson.size) accountsJson(randomPosition) } @@ -66,9 +67,10 @@ trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers { val request = v5_1_0_Request / "banks" / bankId / "accounts" / "private" <@(consumerAndToken) makeGetRequest(request) } - + def randomPrivateAccountViaEndpoint(bankId : String): AccountJSON = { val accountsJson = getPrivateAccountsViaEndpoint(bankId, user1).body.extract[AccountsJSON].accounts + if (accountsJson.isEmpty) throw new IllegalStateException(s"No private accounts found via endpoint for bank $bankId") val randomPosition = nextInt(accountsJson.size) accountsJson(randomPosition) }