From d1a8af416563e1351066c0aa791a06ab46c810b5 Mon Sep 17 00:00:00 2001 From: CristhTejada Date: Sun, 18 Sep 2022 20:37:04 -0600 Subject: [PATCH 1/5] Traduccion inicial del Main-FAQ.html --- obp-api/src/main/resources/i18n/lift-core_es_ES.properties | 6 ++++++ obp-api/src/main/webapp/main-faq.html | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/resources/i18n/lift-core_es_ES.properties b/obp-api/src/main/resources/i18n/lift-core_es_ES.properties index 0723203f9..311278ae5 100644 --- a/obp-api/src/main/resources/i18n/lift-core_es_ES.properties +++ b/obp-api/src/main/resources/i18n/lift-core_es_ES.properties @@ -158,6 +158,12 @@ paginator.displayingrecords = Mostrando %s-%s de %s open_bank_project_is = Open Bank Project es and_commercial_licenses = TESOBE y licencias comerciales +#FAQS +what.is.the.correct = \u00bfCu\u00e1l es la URL base correcta para esta caja de arena (Sandbox)? +The.base.URL.is = La URL base es +Please.make.sure.you.are.using.this.in.all.your.API.calls = Por favor, asegúrese de que utiliza esto en todas sus llamadas a la API + + # Country names country_1 = United States country_2 = Afghanistan diff --git a/obp-api/src/main/webapp/main-faq.html b/obp-api/src/main/webapp/main-faq.html index 769160174..811ef66f1 100644 --- a/obp-api/src/main/webapp/main-faq.html +++ b/obp-api/src/main/webapp/main-faq.html @@ -6,14 +6,14 @@

- The base URL is
+ The base URL is
http://apisandbox.openbankproject.com
- Please make sure you are using this in all your API calls + Please make sure you are using this in all your API calls

From 7a5866f2b68bdf266da281d703679899edfe1730 Mon Sep 17 00:00:00 2001 From: CristhTejada Date: Sun, 18 Sep 2022 20:44:29 -0600 Subject: [PATCH 2/5] Initial translation of Main-FAQ.html --- obp-api/src/main/resources/i18n/lift-core_es_ES.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/i18n/lift-core_es_ES.properties b/obp-api/src/main/resources/i18n/lift-core_es_ES.properties index 033f0431e..ec1d5a7e0 100644 --- a/obp-api/src/main/resources/i18n/lift-core_es_ES.properties +++ b/obp-api/src/main/resources/i18n/lift-core_es_ES.properties @@ -161,7 +161,7 @@ and_commercial_licenses = TESOBE y licencias comerciales #FAQS what.is.the.correct = \u00bfCu\u00e1l es la URL base correcta para esta caja de arena (Sandbox)? The.base.URL.is = La URL base es -Please.make.sure.you.are.using.this.in.all.your.API.calls = Por favor, asegúrese de que utiliza esto en todas sus llamadas a la API +Please.make.sure.you.are.using.this.in.all.your.API.calls = Por favor, aseg\u00farese de que utiliza esto en todas sus llamadas a la API # Country names From acaa642f600fb1406cf78bc56c6c15e5319855cb Mon Sep 17 00:00:00 2001 From: CristhTejada Date: Wed, 21 Sep 2022 08:01:30 -0600 Subject: [PATCH 3/5] Initial translation of Main-FAQ.html --- obp-api/src/main/resources/i18n/lift-core_es_ES.properties | 2 +- obp-api/src/main/webapp/main-faq.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/resources/i18n/lift-core_es_ES.properties b/obp-api/src/main/resources/i18n/lift-core_es_ES.properties index ec1d5a7e0..8e07259dd 100644 --- a/obp-api/src/main/resources/i18n/lift-core_es_ES.properties +++ b/obp-api/src/main/resources/i18n/lift-core_es_ES.properties @@ -159,7 +159,7 @@ open_bank_project_is = Open Bank Project es and_commercial_licenses = TESOBE y licencias comerciales #FAQS -what.is.the.correct = \u00bfCu\u00e1l es la URL base correcta para esta caja de arena (Sandbox)? +what.is.the.correct.base.URL.for.this.instance = \u00bfCu\u00e1l es la URL base correcta para esta caja de arena (Sandbox)? The.base.URL.is = La URL base es Please.make.sure.you.are.using.this.in.all.your.API.calls = Por favor, aseg\u00farese de que utiliza esto en todas sus llamadas a la API diff --git a/obp-api/src/main/webapp/main-faq.html b/obp-api/src/main/webapp/main-faq.html index 811ef66f1..62bb15a8e 100644 --- a/obp-api/src/main/webapp/main-faq.html +++ b/obp-api/src/main/webapp/main-faq.html @@ -6,7 +6,7 @@

From 669d09af7cee0653d33469d0db019b526383f0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 22 Sep 2022 13:18:27 +0200 Subject: [PATCH 4/5] feature/Add endpoint createProduct v5.0.0 --- .../SwaggerDefinitionsJSON.scala | 8 + .../scala/code/api/v5_0_0/APIMethods500.scala | 85 ++++++++- .../code/api/v5_0_0/JSONFactory5.0.0.scala | 11 +- .../scala/code/api/v5_0_0/ProductTest.scala | 169 ++++++++++++++++++ .../code/api/v5_0_0/V500ServerSetup.scala | 27 +++ 5 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/v5_0_0/ProductTest.scala create mode 100644 obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index e2db4eeb5..114df800d 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4623,6 +4623,14 @@ object SwaggerDefinitionsJSON { description = descriptionExample.value, meta = metaJson, ) + val putProductJsonV500 = PutProductJsonV500( + parent_product_code = parentProductCodeExample.value, + name = productNameExample.value, + more_info_url = Some(moreInfoUrlExample.value), + terms_and_conditions_url = Some(termsAndConditionsUrlExample.value), + description = Some(descriptionExample.value), + meta = Some(metaJson) + ) val createMessageJsonV400 = CreateMessageJsonV400( message = messageExample.value, 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 a842c9883..18296a01d 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 @@ -12,6 +12,7 @@ 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, PutProductJsonV400} import code.bankconnectors.Connector import code.consent.{ConsentRequests, Consents} import code.entitlement.Entitlement @@ -21,9 +22,9 @@ import code.views.Views import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.enums.StrongCustomerAuthentication -import com.openbankproject.commons.model.{BankId, CreditLimit, CreditRating, CustomerFaceImage, UserAuthContextUpdateStatus} +import com.openbankproject.commons.model.{BankId, CreditLimit, CreditRating, CustomerFaceImage, ProductCode, UserAuthContextUpdateStatus} import com.openbankproject.commons.util.ApiVersion -import net.liftweb.common.Full +import net.liftweb.common.{Empty, Full} import net.liftweb.http.Req import net.liftweb.http.rest.RestHelper import net.liftweb.json @@ -1019,6 +1020,86 @@ trait APIMethods500 { } } + + staticResourceDocs += ResourceDoc( + createProduct, + implementedInApiVersion, + nameOf(createProduct), + "PUT", + "/banks/BANK_ID/products/PRODUCT_CODE", + "Create Product", + s"""Create or Update Product for the Bank. + | + | + |Typical Super Family values / Asset classes are: + | + |Debt + |Equity + |FX + |Commodity + |Derivative + | + |$productHiearchyAndCollectionNote + | + | + |${authenticationRequiredMessage(true) } + | + | + |""", + putProductJsonV500, + productJsonV400.copy(attributes = None, fees = None), + List( + $UserNotLoggedIn, + $BankNotFound, + UserHasMissingRoles, + UnknownError + ), + List(apiTagProduct, apiTagNewStyle), + Some(List(canCreateProduct, canCreateProductAtAnyBank)) + ) + lazy val createProduct: OBPEndpoint = { + case "banks" :: BankId(bankId) :: "products" :: ProductCode(productCode) :: Nil JsonPut json -> _ => { + 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 $PutProductJsonV400 " + product <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[PutProductJsonV500] + } + parentProductCode <- product.parent_product_code.trim.nonEmpty match { + case false => + Future(Empty) + case true => + Future(Connector.connector.vend.getProduct(bankId, ProductCode(product.parent_product_code))) map { + getFullBoxOrFail(_, callContext, ParentProductNotFoundByProductCode + " {" + product.parent_product_code + "}", 400) + } + } + success <- Future(Connector.connector.vend.createOrUpdateProduct( + bankId = bankId.value, + code = productCode.value, + parentProductCode = parentProductCode.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("") + )) map { + connectorEmptyResponse(_, callContext) + } + } yield { + (JSONFactory400.createProductJson(success), HttpCode.`201`(callContext)) + } + } + } + + + } } diff --git a/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala b/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala index ea8b8f4ef..0596e1e1e 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala @@ -30,7 +30,7 @@ import java.util.Date import code.api.util.APIUtil.stringOrNull import code.api.v1_2_1.BankRoutingJsonV121 -import code.api.v1_4_0.JSONFactory1_4_0.CustomerFaceImageJson +import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, MetaJsonV140} import code.api.v2_1_0.CustomerCreditRatingJSON import code.api.v3_1_0.PostConsentEntitlementJsonV310 import code.api.v4_0_0.BankAttributeBankResponseJsonV400 @@ -80,6 +80,15 @@ case class PostCustomerJsonV500( name_suffix: Option[String] = None ) +case class PutProductJsonV500( + parent_product_code: String, + name: String, + more_info_url: Option[String] = None, + terms_and_conditions_url: Option[String] = None, + description: Option[String] = None, + meta: Option[MetaJsonV140] = None, +) + case class UserAuthContextJsonV500( user_auth_context_id: String, user_id: String, diff --git a/obp-api/src/test/scala/code/api/v5_0_0/ProductTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/ProductTest.scala new file mode 100644 index 000000000..324d9793a --- /dev/null +++ b/obp-api/src/test/scala/code/api/v5_0_0/ProductTest.scala @@ -0,0 +1,169 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH +Osloerstrasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + */ +package code.api.v5_0_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.util.ApiRole._ +import code.api.util.APIUtil.OAuth._ +import code.api.util.ErrorMessages._ +import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0 +import code.api.v5_0_0.OBPAPI5_0_0.Implementations5_0_0 +import code.api.v4_0_0.{ProductJsonV400, ProductsJsonV400} +import code.entitlement.Entitlement +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + +import scala.collection.immutable.Nil + +class ProductTest extends V500ServerSetup { + + override def beforeAll(): Unit = { + super.beforeAll() + } + + override def afterAll(): Unit = { + super.afterAll() + } + + /** + * Test tags + * Example: To run tests with tag "getPermissions": + * mvn test -D tagsToInclude + * + * This is made possible by the scalatest maven plugin + */ + object VersionOfApi extends Tag(ApiVersion.v5_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations5_0_0.createProduct)) + object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getProduct)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getProducts)) + + lazy val testBankId = randomBankId + lazy val parentPutProductJsonV500: PutProductJsonV500 = SwaggerDefinitionsJSON.putProductJsonV500.copy(parent_product_code ="") + def createProduct(code: String, json: PutProductJsonV500): ProductJsonV400 = { + When("We try to create a product v4.0.0") + val request500 = (v5_0_0_Request / "banks" / testBankId / "products" / code).PUT <@ (user1) + val response500 = makePutRequest(request500, write(json)) + Then("We should get a 201") + response500.code should equal(201) + val product = response500.body.extract[ProductJsonV400] + product.product_code shouldBe code + product.parent_product_code shouldBe json.parent_product_code + product.bank_id shouldBe testBankId + product.name shouldBe json.name + product.more_info_url shouldBe json.more_info_url.getOrElse("") + product.terms_and_conditions_url shouldBe json.terms_and_conditions_url.getOrElse("") + product.description shouldBe json.description.getOrElse("") + product + } + + feature("Create Product v4.0.0") { + scenario("We will call the Add endpoint without a user credentials", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v5_0_0_Request / "banks" / testBankId / "products" / "CODE").PUT + val response400 = makePutRequest(request400, write(parentPutProductJsonV500)) + Then("We should get a 401") + response400.code should equal(401) + And("error should be " + UserNotLoggedIn) + response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) + } + scenario("We will call the Add endpoint without a proper role", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + val request500 = (v5_0_0_Request / "banks" / testBankId / "products" / "CODE").PUT <@(user1) + val response500 = makePutRequest(request500, write(parentPutProductJsonV500)) + Then("We should get a 403") + response500.code should equal(403) + val createProductEntitlements = canCreateProduct :: canCreateProductAtAnyBank :: Nil + val createProductEntitlementsRequiredText = UserHasMissingRoles + createProductEntitlements.mkString(" or ") + And("error should be " + createProductEntitlementsRequiredText) + response500.body.extract[ErrorMessage].message contains (createProductEntitlementsRequiredText) should be (true) + } + scenario("We will call the Add endpoint with user credentials and role", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, CanCreateProduct.toString) + + // Create an grandparent + val grandparent: ProductJsonV400 = createProduct(code = "GRANDPARENT_CODE", json = parentPutProductJsonV500) + + // Create an parent + val product: ProductJsonV400 = createProduct(code = "PARENT_CODE", json = parentPutProductJsonV500.copy(parent_product_code = grandparent.product_code)) + + // Get + val requestGet400 = (v5_0_0_Request / "banks" / product.bank_id / "products" / product.product_code ).GET <@(user1) + val responseGet400 = makeGetRequest(requestGet400) + Then("We should get a 200") + responseGet400.code should equal(200) + val product1 = responseGet400.body.extract[ProductJsonV400] + + // Create an child + val childPutProductJsonV400 = parentPutProductJsonV500.copy(parent_product_code = product.product_code) + createProduct(code = "PRODUCT_CODE", json = childPutProductJsonV400) + + // Get + val requestGetAll400 = (v5_0_0_Request / "banks" / product.bank_id / "products").GET <@(user1) + val responseGetAll400 = makeGetRequest(requestGetAll400) + Then("We should get a 200") + responseGetAll400.code should equal(200) + val products: ProductsJsonV400 = responseGetAll400.body.extract[ProductsJsonV400] + products.products.size shouldBe 3 + } + scenario("We will call the Add endpoint with user credentials and role and minimal PUT JSON", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, CanCreateProduct.toString) + // Create an grandparent + val grandparent: ProductJsonV400 = createProduct( + code = "GRANDPARENT_CODE", + json = PutProductJsonV500( + name = parentPutProductJsonV500.name, + parent_product_code = parentPutProductJsonV500.parent_product_code + ) + ) + // Create an parent + val product: ProductJsonV400 = createProduct(code = "PARENT_CODE", json = parentPutProductJsonV500.copy(parent_product_code = grandparent.product_code)) + + // Get + val requestGet400 = (v5_0_0_Request / "banks" / product.bank_id / "products" / product.product_code ).GET <@(user1) + val responseGet400 = makeGetRequest(requestGet400) + Then("We should get a 200") + responseGet400.code should equal(200) + val product1 = responseGet400.body.extract[ProductJsonV400] + + // Create an child + val childPutProductJsonV400 = parentPutProductJsonV500.copy(parent_product_code = product.product_code) + createProduct(code = "PRODUCT_CODE", json = childPutProductJsonV400) + + // Get + val requestGetAll400 = (v5_0_0_Request / "banks" / product.bank_id / "products").GET <@(user1) + val responseGetAll400 = makeGetRequest(requestGetAll400) + Then("We should get a 200") + responseGetAll400.code should equal(200) + val products: ProductsJsonV400 = responseGetAll400.body.extract[ProductsJsonV400] + products.products.size shouldBe 3 + } + } + + +} 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 new file mode 100644 index 000000000..890112dbd --- /dev/null +++ b/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala @@ -0,0 +1,27 @@ +package code.api.v5_0_0 + +import code.api.v4_0_0.BanksJson400 +import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} +import com.openbankproject.commons.util.ApiShortVersions +import dispatch.Req + +import scala.util.Random.nextInt + +trait V500ServerSetup extends ServerSetupWithTestData with DefaultUsers { + + def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" + def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString + def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString + + def randomBankId : String = { + def getBanksInfo : APIResponse = { + val request = v5_0_0_Request / "banks" + makeGetRequest(request) + } + val banksJson = getBanksInfo.body.extract[BanksJson400] + val randomPosition = nextInt(banksJson.banks.size) + val bank = banksJson.banks(randomPosition) + bank.id + } + +} \ No newline at end of file From 7520a6d9da4118e6d3122c611aa62516146d7a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 23 Sep 2022 11:30:13 +0200 Subject: [PATCH 5/5] feature/Add endpoint createAccount v5.0.0 --- .../SwaggerDefinitionsJSON.scala | 8 + .../scala/code/api/v5_0_0/APIMethods500.scala | 143 +++++++++- .../code/api/v5_0_0/JSONFactory5.0.0.scala | 9 + .../scala/code/api/v5_0_0/AccountTest.scala | 244 ++++++++++++++++++ 4 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/v5_0_0/AccountTest.scala diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 114df800d..0f5c9ed9c 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4102,6 +4102,14 @@ object SwaggerDefinitionsJSON { branch_id = branchIdExample.value, account_routings = List(accountRoutingJsonV121) ) + val createAccountRequestJsonV500 = CreateAccountRequestJsonV500( + user_id = Some(userIdExample.value), + label = labelExample.value, + product_code = productCodeExample.value, + balance = Some(amountOfMoneyJsonV121), + branch_id = Some(branchIdExample.value), + account_routings = Some(List(accountRoutingJsonV121)) + ) val settlementAccountRequestJson = SettlementAccountRequestJson( user_id = userIdExample.value, 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 18296a01d..1c4c35877 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 @@ -16,19 +16,20 @@ import code.api.v4_0_0.{JSONFactory400, PutProductJsonV400} import code.bankconnectors.Connector import code.consent.{ConsentRequests, Consents} import code.entitlement.Entitlement +import code.model.dataAccess.BankAccountCreation import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _} import code.util.Helper import code.views.Views import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.enums.StrongCustomerAuthentication -import com.openbankproject.commons.model.{BankId, CreditLimit, CreditRating, CustomerFaceImage, ProductCode, UserAuthContextUpdateStatus} +import com.openbankproject.commons.model.{AccountId, AccountRouting, BankId, CreditLimit, CreditRating, CustomerFaceImage, ProductCode, UserAuthContextUpdateStatus} 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.compactRender +import net.liftweb.json.{Extraction, compactRender, prettyRender} import net.liftweb.util.Helpers.tryo import net.liftweb.util.Props @@ -243,6 +244,144 @@ trait APIMethods500 { } } } + + + resourceDocs += ResourceDoc( + createAccount, + implementedInApiVersion, + "createAccount", + "PUT", + "/banks/BANK_ID/accounts/ACCOUNT_ID", + "Create Account", + """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, + UserNotLoggedIn, + InvalidUserId, + InvalidAccountIdFormat, + InvalidBankIdFormat, + UserNotFoundById, + UserHasMissingRoles, + InvalidAccountBalanceAmount, + InvalidAccountInitialBalance, + InitialBalanceMustBeZero, + InvalidAccountBalanceCurrency, + AccountIdAlreadyExists, + UnknownError + ), + List(apiTagAccount,apiTagOnboarding, apiTagNewStyle), + Some(List(canCreateAccount)) + ) + + + lazy val createAccount : OBPEndpoint = { + // Create a new account + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonPut json -> _ => { + 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] + ) + } yield { + //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) + (JSONFactory310.createAccountJSON(userIdAccountOwner, bankAccount, accountAttributes), HttpCode.`201`(callContext)) + } + } + } + } staticResourceDocs += ResourceDoc( diff --git a/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala b/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala index 0596e1e1e..95a940a77 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala @@ -60,6 +60,15 @@ case class BankJson500( attributes: Option[List[BankAttributeBankResponseJsonV400]] ) +case class CreateAccountRequestJsonV500( + user_id : Option[String], + label : String, + product_code : String, + balance : Option[AmountOfMoneyJsonV121], + branch_id : Option[String], + account_routings: Option[List[AccountRoutingJsonV121]] +) + case class PostCustomerJsonV500( legal_name: String, mobile_phone_number: String, diff --git a/obp-api/src/test/scala/code/api/v5_0_0/AccountTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/AccountTest.scala new file mode 100644 index 000000000..4ce148c18 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v5_0_0/AccountTest.scala @@ -0,0 +1,244 @@ +package code.api.v5_0_0 + +import java.util.UUID +import java.util.concurrent.TimeUnit + +import code.api.Constant +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.util.APIUtil.OAuth._ +import code.api.util.APIUtil.extractErrorMessageCode +import code.api.util.ApiRole +import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} +import code.api.v2_0_0.BasicAccountJSON +import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 +import code.api.v3_0_0.CoreAccountsJsonV300 +import code.api.v3_0_0.OBPAPI3_0_0.Implementations3_0_0 +import code.api.v3_1_0.CreateAccountResponseJsonV310 +import code.api.v4_0_0.{AccountsBalancesJsonV400, ModeratedCoreAccountJsonV400} +import code.api.v5_0_0.OBPAPI5_0_0.Implementations5_0_0 +import code.entitlement.Entitlement +import code.setup.DefaultUsers +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.enums.AccountRoutingScheme +import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, ErrorMessage} +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + +import scala.util.Random + +class AccountTest extends V500ServerSetup with DefaultUsers { + + object VersionOfApi extends Tag(ApiVersion.v5_0_0.toString) + object ApiEndpoint2 extends Tag(nameOf(Implementations5_0_0.createAccount)) + //We need this endpoint to test the result + object ApiEndpoint4 extends Tag(nameOf(Implementations3_0_0.corePrivateAccountsAllBanks)) + object ApiEndpoint5 extends Tag(nameOf(Implementations2_0_0.getPrivateAccountsAtOneBank)) + object ApiEndpoint6 extends Tag(nameOf(Implementations3_0_0.getPrivateAccountById)) + + lazy val testBankId = testBankId1 + lazy val putCreateAccountJSONV310 = SwaggerDefinitionsJSON.createAccountRequestJsonV310.copy(user_id = resourceUser1.userId, balance = AmountOfMoneyJsonV121("EUR","0")) + lazy val putCreateAccountOtherUserJsonV310 = SwaggerDefinitionsJSON.createAccountRequestJsonV310 + .copy(user_id = resourceUser2.userId, balance = AmountOfMoneyJsonV121("EUR","0"), + account_routings = List(AccountRoutingJsonV121(Random.nextString(10), Random.nextString(10)))) + + val userAccountId = UUID.randomUUID.toString + val user2AccountId = UUID.randomUUID.toString + + + feature(s"Create Account $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint2, VersionOfApi) { + When(s"We make a request $VersionOfApi") + val request310 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "ACCOUNT_ID" ).PUT + val response310 = makePutRequest(request310, write(putCreateAccountJSONV310)) + Then("We should get a 401") + response310.code should equal(401) + And("error should be " + UserNotLoggedIn) + response310.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) + } + } + feature(s"Create Account $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials", ApiEndpoint2, VersionOfApi) { + When(s"We make a request $VersionOfApi") + Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString()) + val request = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "TEST_ACCOUNT_ID" ).PUT <@(user1) + val response = makePutRequest(request, write(putCreateAccountJSONV310)) + Then("We should get a 201") + response.code should equal(201) + //for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method, + //it is an asynchronous process, need some time to be done. + TimeUnit.SECONDS.sleep(2) + + + val account = response.body.extract[CreateAccountResponseJsonV310] + account.product_code should be (putCreateAccountJSONV310.product_code) + account.`label` should be (putCreateAccountJSONV310.`label`) + account.balance.amount.toDouble should be (putCreateAccountJSONV310.balance.amount.toDouble) + account.balance.currency should be (putCreateAccountJSONV310.balance.currency) + account.branch_id should be (putCreateAccountJSONV310.branch_id) + account.user_id should be (putCreateAccountJSONV310.user_id) + account.label should be (putCreateAccountJSONV310.label) + account.account_routings should be (putCreateAccountJSONV310.account_routings) + + + //We need to waite some time for the account creation, because we introduce `AuthUser.refreshUser(user, callContext)` + //It may not finished when we call the get accounts directly. + TimeUnit.SECONDS.sleep(2) + + Then(s"we call $ApiEndpoint4 to get the account back") + val requestApiEndpoint4 = (v5_0_0_Request / "my" / "accounts" ).PUT <@(user1) + val responseApiEndpoint4 = makeGetRequest(requestApiEndpoint4) + + + + responseApiEndpoint4.code should equal(200) + val accounts = responseApiEndpoint4.body.extract[CoreAccountsJsonV300].accounts + accounts.map(_.id).toList.toString() contains(account.account_id) should be (true) + + Then(s"we call $ApiEndpoint5 to get the account back") + val requestApiEndpoint5 = (v5_0_0_Request /"banks" / testBankId.value / "accounts").GET <@ (user1) + val responseApiEndpoint5 = makeGetRequest(requestApiEndpoint5) + + Then("We should get a 200") + responseApiEndpoint5.code should equal(200) + responseApiEndpoint5.body.extract[List[BasicAccountJSON]].toList.toString() contains(account.account_id) should be (true) + + + val requestGetApiEndpoint3 = (v5_0_0_Request / "banks" / testBankId.value / "balances").GET <@ (user1) + val responseGetApiEndpoint3 = makeGetRequest(requestGetApiEndpoint3) + responseGetApiEndpoint3.code should equal(200) + responseGetApiEndpoint3.body.extract[AccountsBalancesJsonV400].accounts.map(_.account_id) contains(account.account_id) should be (true) + + + Then(s"We make a request $VersionOfApi but with other user") + val request500WithNewAccountId = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "TEST_ACCOUNT_ID2" ).PUT <@(user2) + val responseWithNoRole = makePutRequest(request500WithNewAccountId, write(putCreateAccountOtherUserJsonV310)) + Then("We should get a 403 and some error message") + responseWithNoRole.code should equal(403) + responseWithNoRole.body.toString contains(extractErrorMessageCode(UserHasMissingRoles)) should be (true) + + + Then("We grant the roles and test it again") + Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser2.userId, ApiRole.canCreateAccount.toString) + val responseWithOtherUserV500 = makePutRequest(request500WithNewAccountId, write(putCreateAccountOtherUserJsonV310)) + + val account2 = responseWithOtherUserV500.body.extract[CreateAccountResponseJsonV310] + account2.product_code should be (putCreateAccountOtherUserJsonV310.product_code) + account2.`label` should be (putCreateAccountOtherUserJsonV310.`label`) + account2.balance.amount.toDouble should be (putCreateAccountOtherUserJsonV310.balance.amount.toDouble) + account2.balance.currency should be (putCreateAccountOtherUserJsonV310.balance.currency) + account2.branch_id should be (putCreateAccountOtherUserJsonV310.branch_id) + account2.user_id should be (putCreateAccountOtherUserJsonV310.user_id) + account2.label should be (putCreateAccountOtherUserJsonV310.label) + account2.account_routings should be (putCreateAccountOtherUserJsonV310.account_routings) + + } + + scenario("Create new account will have system owner view, and other use also have the system owner view should not get the account back", ApiEndpoint2, VersionOfApi) { + When(s"We make a request $VersionOfApi") + Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString) + val request500 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / userAccountId ).PUT <@(user1) + val putCreateAccountJson = putCreateAccountJSONV310.copy(account_routings = List(AccountRoutingJsonV121("AccountNumber", "15649885656"))) + val response500 = makePutRequest(request500, write(putCreateAccountJson)) + Then("We should get a 201") + response500.code should equal(201) + //for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method, + //it is an asynchronous process, need some time to be done. + TimeUnit.SECONDS.sleep(2) + + val account = response500.body.extract[CreateAccountResponseJsonV310] + account.product_code should be (putCreateAccountJson.product_code) + account.`label` should be (putCreateAccountJson.`label`) + account.balance.amount.toDouble should be (putCreateAccountJson.balance.amount.toDouble) + account.balance.currency should be (putCreateAccountJson.balance.currency) + account.branch_id should be (putCreateAccountJson.branch_id) + account.user_id should be (putCreateAccountJson.user_id) + account.label should be (putCreateAccountJson.label) + account.account_routings should be (putCreateAccountJson.account_routings) + + + Then(s"we call $ApiEndpoint6 to get the account back") + val requestApiEndpoint6 = (v5_0_0_Request /"banks" / testBankId.value / "accounts" / userAccountId / Constant.SYSTEM_OWNER_VIEW_ID/ "account" ).GET <@(user1) + val responseApiEndpoint6 = makeGetRequest(requestApiEndpoint6) + + responseApiEndpoint6.code should equal(200) + val accountEndpoint6 = responseApiEndpoint6.body.extract[ModeratedCoreAccountJsonV400] + accountEndpoint6.id should be (userAccountId) + accountEndpoint6.label should be (account.label) + + Then(s"we prepare the user2 will create a new account ($ApiEndpoint2)and he will have system view, and to call get account ($ApiEndpoint6) and compare the result.") + Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser2.userId, ApiRole.canCreateAccount.toString) + val requestUser2_500 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / user2AccountId ).PUT <@(user2) + val responseUser2_500 = makePutRequest(requestUser2_500, write(putCreateAccountJSONV310.copy(user_id = resourceUser2.userId, balance = AmountOfMoneyJsonV121("EUR","0")))) + Then("We should get a 201") + responseUser2_500.code should equal(201) + + //for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method, + //it is an asynchronous process, need some time to be done. + TimeUnit.SECONDS.sleep(2) + + + Then(s"we call $ApiEndpoint6 to get the account back by user2") + val requestApiUser2Endpoint6 = (v5_0_0_Request /"banks" / testBankId.value / "accounts" / userAccountId / Constant.SYSTEM_OWNER_VIEW_ID/ "account" ).GET <@(user2) + val responseApiUser2Endpoint6 = makeGetRequest(requestApiUser2Endpoint6) + //This mean, the user2 can not get access to user1's account! + responseApiUser2Endpoint6.code should not equal(200) + + } + + scenario("Create new account with an already existing routing scheme/address should not create the account", ApiEndpoint2, VersionOfApi) { + When(s"We make a request $VersionOfApi to create the first account") + Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString) + val request310_1 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "TEST_ACCOUNT_ID_1" ).PUT <@(user1) + val response310_1 = makePutRequest(request310_1, write(putCreateAccountJSONV310)) + Then("We should get a 201") + response310_1.code should equal(201) + + //for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method, + //it is an asynchronous process, need some time to be done. + TimeUnit.SECONDS.sleep(2) + val account = response310_1.body.extract[CreateAccountResponseJsonV310] + account.product_code should be (putCreateAccountJSONV310.product_code) + account.`label` should be (putCreateAccountJSONV310.`label`) + account.balance.amount.toDouble should be (putCreateAccountJSONV310.balance.amount.toDouble) + account.balance.currency should be (putCreateAccountJSONV310.balance.currency) + account.branch_id should be (putCreateAccountJSONV310.branch_id) + account.user_id should be (putCreateAccountJSONV310.user_id) + account.label should be (putCreateAccountJSONV310.label) + account.account_routings should be (putCreateAccountJSONV310.account_routings) + + When(s"We make a request $VersionOfApi to create the second account with an already existing scheme/address") + val request310_2 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / user2AccountId ).PUT <@(user1) + val response310_2 = makePutRequest(request310_2, write(putCreateAccountJSONV310)) + Then("We should get a 400 in the createAccount response") + response310_2.code should equal(400) + response310_2.body.toString should include("OBP-30115: Account Routing already exist.") + + Then(s"The second account should not be created") + val requestApiGetAccount = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / user2AccountId / Constant.SYSTEM_OWNER_VIEW_ID / "account" ).GET <@(user1) + val responseApiGetAccount = makeGetRequest(requestApiGetAccount) + And("We should get a 404 in the getAccount response") + responseApiGetAccount.code should equal(404) + } + + scenario("Create new account with a duplication in routing scheme should not create the account", ApiEndpoint2, VersionOfApi) { + When(s"We make a request $VersionOfApi to create the account") + Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString) + val request500 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / userAccountId ).PUT <@(user1) + val putCreateAccountJsonWithRoutingSchemeDuplication = putCreateAccountJSONV310.copy(account_routings = + List(AccountRoutingJsonV121(AccountRoutingScheme.IBAN.toString, Random.nextString(10)), + AccountRoutingJsonV121(AccountRoutingScheme.IBAN.toString, Random.nextString(10)))) + val response500 = makePutRequest(request500, write(putCreateAccountJsonWithRoutingSchemeDuplication)) + Then("We should get a 400 in the createAccount response") + response500.code should equal(400) + response500.body.toString should include ("Duplication detected in account routings, please specify only one value per routing scheme") + + Then(s"The account should not be created") + val requestApiGetAccount = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / userAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "account" ).GET <@(user1) + val responseApiGetAccount = makeGetRequest(requestApiGetAccount) + And("We should get a 404 in the getAccount response") + responseApiGetAccount.code should equal(404) + } + } + +}