From 7aad850f8a88cfcb3b318e3c1972f85033f7f785 Mon Sep 17 00:00:00 2001 From: Everett Sochowski Date: Wed, 4 Mar 2015 17:28:19 +0100 Subject: [PATCH 001/702] Modify files to avoid automatic merge when resyncing with develop --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- src/main/scala/code/api/sandbox/SandboxApiCalls.scala | 2 +- src/main/scala/code/bankconnectors/Connector.scala | 2 +- src/main/scala/code/bankconnectors/LocalConnector.scala | 2 +- src/main/scala/code/bankconnectors/LocalMappedConnector.scala | 2 +- .../code/model/dataAccess/BankAccountCreationDispatcher.scala | 2 +- src/main/scala/code/sandbox/CreateOBPUsers.scala | 2 +- src/main/scala/code/sandbox/CreateViewImpls.scala | 2 +- src/main/scala/code/sandbox/LocalConnectorDataImport.scala | 2 +- .../scala/code/sandbox/LocalMappedConnectorDataImport.scala | 2 +- src/main/scala/code/sandbox/OBPDataImport.scala | 2 +- src/main/scala/code/snippet/CreateTestAccountForm.scala | 2 +- src/main/scala/code/tesobe/CashAPI.scala | 2 +- src/main/scala/code/tesobe/ImporterAPI.scala | 2 +- src/main/scala/code/tesobe/TransactionInserter.scala | 2 +- src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala | 2 +- .../bankaccountcreation/BankAccountCreationListenerTest.scala | 2 +- .../code/bankaccountcreation/BankAccountCreationTest.scala | 2 +- src/test/scala/code/sandbox/SandboxDataLoadingTest.scala | 2 +- src/test/scala/code/tesobe/CashAPITest.scala | 2 +- src/test/scala/code/tesobe/ImporterTest.scala | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 74e0fc8ce..63bb8a4a3 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -192,7 +192,7 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(OAuthHandshake) //add sandbox api calls only if we're running in sandbox mode - if(Props.getBool("allow_sandbox_data_import", false)) { + if(Props.getBool("allow_sandbox_data_import", false)) { logger.info("Adding sandbox api calls") LiftRules.statelessDispatch.append(SandboxApiCalls) } else { diff --git a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala index 1c72f2d27..ac988e42f 100644 --- a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala +++ b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala @@ -1,4 +1,4 @@ -package code.api.sandbox + package code.api.sandbox import code.api.{APIFailure, OBPRestHelper} import code.api.util.APIUtil._ diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 6cfbb673b..274334979 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -99,7 +99,7 @@ trait Connector { protected def makePaymentImpl(fromAccount : AccountType, toAccount : AccountType, amt : BigDecimal) : Box[TransactionId] - //non-standard calls --do not make sense in the regular context + //non-standard calls --do not make sense in the regular context //creates a bank account (if it doesn't exist) and creates a bank (if it doesn't exist) def createBankAndAccount(bankName : String, bankNationalIdentifier : String, accountNumber : String, accountHolderName : String) : (Bank, BankAccount) diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index c7a24d82f..e5674f8cd 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -325,7 +325,7 @@ private object LocalConnector extends Connector with Loggable { } //Need to pass in @hostedBank because the Account model doesn't have any references to BankId, just to the mongo id of the Bank object (which itself does have the bank id) - private def createAccount(hostedBank : HostedBank, accountId : AccountId, accountNumber: String, currency : String, initialBalance : BigDecimal, holderName : String) : BankAccount = { + private def createAccount(hostedBank : HostedBank, accountId : AccountId, accountNumber: String, currency : String, initialBalance : BigDecimal, holderName : String) : BankAccount = { import net.liftweb.mongodb.BsonDSL._ Account.find( (Account.accountNumber.name -> accountNumber)~ diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 226c8ab0c..78a653fc5 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -199,7 +199,7 @@ object LocalMappedConnector extends Connector with Loggable { //creates a bank account (if it doesn't exist) and creates a bank (if it doesn't exist) //again assume national identifier is unique - override def createBankAndAccount(bankName: String, bankNationalIdentifier: String, accountNumber: String, accountHolderName: String): (Bank, BankAccount) = { + override def createBankAndAccount(bankName: String, bankNationalIdentifier: String, accountNumber: String, accountHolderName: String): (Bank, BankAccount) = { //don't require and exact match on the name, just the identifier val bank = MappedBank.find(By(MappedBank.national_identifier, bankNationalIdentifier)) match { case Full(b) => diff --git a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala index 95604f67c..371c27821 100644 --- a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala +++ b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala @@ -36,7 +36,7 @@ Berlin 13359, Germany * so that the API create an Bank (if necessary), * the bank account and an owner view. */ -package com.tesobe.model{ + package com.tesobe.model{ case class CreateBankAccount( accountOwnerId: String, accountOwnerProvider: String, diff --git a/src/main/scala/code/sandbox/CreateOBPUsers.scala b/src/main/scala/code/sandbox/CreateOBPUsers.scala index 59d64482d..65da56053 100644 --- a/src/main/scala/code/sandbox/CreateOBPUsers.scala +++ b/src/main/scala/code/sandbox/CreateOBPUsers.scala @@ -1,4 +1,4 @@ -package code.sandbox + package code.sandbox import code.model.dataAccess.{OBPUser, APIUser} import net.liftweb.common.{Full, Failure, Box} diff --git a/src/main/scala/code/sandbox/CreateViewImpls.scala b/src/main/scala/code/sandbox/CreateViewImpls.scala index 595f074c4..c156ca3c2 100644 --- a/src/main/scala/code/sandbox/CreateViewImpls.scala +++ b/src/main/scala/code/sandbox/CreateViewImpls.scala @@ -1,4 +1,4 @@ -package code.sandbox + package code.sandbox import code.model.{AccountId, BankId} import code.model.dataAccess.ViewImpl diff --git a/src/main/scala/code/sandbox/LocalConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalConnectorDataImport.scala index 798b4b704..0ff5f2175 100644 --- a/src/main/scala/code/sandbox/LocalConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalConnectorDataImport.scala @@ -1,4 +1,4 @@ -package code.sandbox + package code.sandbox import code.metadata.counterparties.{MongoCounterparties, Metadata} import code.model._ diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index a55515665..afaa9829c 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -1,4 +1,4 @@ -package code.sandbox + package code.sandbox import code.metadata.counterparties.{MappedCounterpartyMetadata} import code.model.dataAccess.{MappedBankAccount, MappedBank} diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index e0555e700..65c02ce58 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -1,4 +1,4 @@ -package code.sandbox + package code.sandbox import java.text.SimpleDateFormat import java.util.UUID diff --git a/src/main/scala/code/snippet/CreateTestAccountForm.scala b/src/main/scala/code/snippet/CreateTestAccountForm.scala index b6232acb8..0c96b4e5a 100644 --- a/src/main/scala/code/snippet/CreateTestAccountForm.scala +++ b/src/main/scala/code/snippet/CreateTestAccountForm.scala @@ -1,4 +1,4 @@ -package code.snippet + package code.snippet import code.bankconnectors.Connector import net.liftweb.util.Helpers._ diff --git a/src/main/scala/code/tesobe/CashAPI.scala b/src/main/scala/code/tesobe/CashAPI.scala index 1fd178939..80999fe56 100644 --- a/src/main/scala/code/tesobe/CashAPI.scala +++ b/src/main/scala/code/tesobe/CashAPI.scala @@ -1,4 +1,4 @@ -package code.tesobe + package code.tesobe import java.util.Date import code.bankconnectors.Connector diff --git a/src/main/scala/code/tesobe/ImporterAPI.scala b/src/main/scala/code/tesobe/ImporterAPI.scala index 37bfc6e5d..584789ff3 100644 --- a/src/main/scala/code/tesobe/ImporterAPI.scala +++ b/src/main/scala/code/tesobe/ImporterAPI.scala @@ -1,4 +1,4 @@ -package code.tesobe + package code.tesobe import java.util.Date diff --git a/src/main/scala/code/tesobe/TransactionInserter.scala b/src/main/scala/code/tesobe/TransactionInserter.scala index 391200fce..92344e4c0 100644 --- a/src/main/scala/code/tesobe/TransactionInserter.scala +++ b/src/main/scala/code/tesobe/TransactionInserter.scala @@ -1,4 +1,4 @@ -package code.tesobe + package code.tesobe import code.bankconnectors.Connector import code.model.Transaction diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index b9947c527..bcde43119 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -96,7 +96,7 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { protected def makePaymentImpl(fromAccount : AccountType, toAccount : AccountType, amt : BigDecimal) : Box[TransactionId] = Failure("not supported") - override def createBankAndAccount(bankName : String, bankNationalIdentifier : String, + override def createBankAndAccount(bankName : String, bankNationalIdentifier : String, accountNumber : String, accountHolderName : String): (Bank, BankAccount) = ??? //sets a user as an account owner/holder diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala index 434c1ff76..3a05962ac 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala @@ -1,4 +1,4 @@ -package code.bankaccountcreation + package code.bankaccountcreation import code.api.DefaultConnectorTestSetup import code.api.test.ServerSetup diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala index 2f05a9715..74670067c 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala @@ -1,4 +1,4 @@ -package code.bankaccountcreation + package code.bankaccountcreation import code.api.test.ServerSetup import code.api.{DefaultConnectorTestSetup, DefaultUsers} diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 8f351b4c4..a99c607f8 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -29,7 +29,7 @@ Berlin 13359, Germany Ayoub Benali: ayoub AT tesobe DOT com */ -package code.sandbox + package code.sandbox import java.util.Date diff --git a/src/test/scala/code/tesobe/CashAPITest.scala b/src/test/scala/code/tesobe/CashAPITest.scala index f6afc9e57..e3293ec79 100644 --- a/src/test/scala/code/tesobe/CashAPITest.scala +++ b/src/test/scala/code/tesobe/CashAPITest.scala @@ -1,4 +1,4 @@ -package code.tesobe + package code.tesobe import java.util.{UUID, Date} import net.liftweb.json.Serialization.write diff --git a/src/test/scala/code/tesobe/ImporterTest.scala b/src/test/scala/code/tesobe/ImporterTest.scala index 41760f3af..ca7456757 100644 --- a/src/test/scala/code/tesobe/ImporterTest.scala +++ b/src/test/scala/code/tesobe/ImporterTest.scala @@ -1,4 +1,4 @@ -package code.tesobe + package code.tesobe import java.text.SimpleDateFormat import java.util.TimeZone From 281e6484498901e1f9a5ee7ca2af326e4a715fb6 Mon Sep 17 00:00:00 2001 From: Everett Sochowski Date: Wed, 4 Mar 2015 17:34:34 +0100 Subject: [PATCH 002/702] Remove the modifications made to induce merge conflict --- src/main/scala/code/api/sandbox/SandboxApiCalls.scala | 2 +- .../code/model/dataAccess/BankAccountCreationDispatcher.scala | 2 +- src/main/scala/code/sandbox/CreateOBPUsers.scala | 2 +- src/main/scala/code/sandbox/CreateViewImpls.scala | 2 +- src/main/scala/code/sandbox/LocalConnectorDataImport.scala | 2 +- .../scala/code/sandbox/LocalMappedConnectorDataImport.scala | 2 +- src/main/scala/code/sandbox/OBPDataImport.scala | 2 +- src/main/scala/code/snippet/CreateTestAccountForm.scala | 2 +- src/main/scala/code/tesobe/CashAPI.scala | 2 +- src/main/scala/code/tesobe/ImporterAPI.scala | 2 +- src/main/scala/code/tesobe/TransactionInserter.scala | 2 +- .../bankaccountcreation/BankAccountCreationListenerTest.scala | 2 +- .../code/bankaccountcreation/BankAccountCreationTest.scala | 2 +- src/test/scala/code/sandbox/SandboxDataLoadingTest.scala | 2 +- src/test/scala/code/tesobe/CashAPITest.scala | 2 +- src/test/scala/code/tesobe/ImporterTest.scala | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala index ac988e42f..1c72f2d27 100644 --- a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala +++ b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala @@ -1,4 +1,4 @@ - package code.api.sandbox +package code.api.sandbox import code.api.{APIFailure, OBPRestHelper} import code.api.util.APIUtil._ diff --git a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala index 371c27821..95604f67c 100644 --- a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala +++ b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala @@ -36,7 +36,7 @@ Berlin 13359, Germany * so that the API create an Bank (if necessary), * the bank account and an owner view. */ - package com.tesobe.model{ +package com.tesobe.model{ case class CreateBankAccount( accountOwnerId: String, accountOwnerProvider: String, diff --git a/src/main/scala/code/sandbox/CreateOBPUsers.scala b/src/main/scala/code/sandbox/CreateOBPUsers.scala index 65da56053..59d64482d 100644 --- a/src/main/scala/code/sandbox/CreateOBPUsers.scala +++ b/src/main/scala/code/sandbox/CreateOBPUsers.scala @@ -1,4 +1,4 @@ - package code.sandbox +package code.sandbox import code.model.dataAccess.{OBPUser, APIUser} import net.liftweb.common.{Full, Failure, Box} diff --git a/src/main/scala/code/sandbox/CreateViewImpls.scala b/src/main/scala/code/sandbox/CreateViewImpls.scala index c156ca3c2..595f074c4 100644 --- a/src/main/scala/code/sandbox/CreateViewImpls.scala +++ b/src/main/scala/code/sandbox/CreateViewImpls.scala @@ -1,4 +1,4 @@ - package code.sandbox +package code.sandbox import code.model.{AccountId, BankId} import code.model.dataAccess.ViewImpl diff --git a/src/main/scala/code/sandbox/LocalConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalConnectorDataImport.scala index 0ff5f2175..798b4b704 100644 --- a/src/main/scala/code/sandbox/LocalConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalConnectorDataImport.scala @@ -1,4 +1,4 @@ - package code.sandbox +package code.sandbox import code.metadata.counterparties.{MongoCounterparties, Metadata} import code.model._ diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index afaa9829c..a55515665 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -1,4 +1,4 @@ - package code.sandbox +package code.sandbox import code.metadata.counterparties.{MappedCounterpartyMetadata} import code.model.dataAccess.{MappedBankAccount, MappedBank} diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 65c02ce58..e0555e700 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -1,4 +1,4 @@ - package code.sandbox +package code.sandbox import java.text.SimpleDateFormat import java.util.UUID diff --git a/src/main/scala/code/snippet/CreateTestAccountForm.scala b/src/main/scala/code/snippet/CreateTestAccountForm.scala index 0c96b4e5a..b6232acb8 100644 --- a/src/main/scala/code/snippet/CreateTestAccountForm.scala +++ b/src/main/scala/code/snippet/CreateTestAccountForm.scala @@ -1,4 +1,4 @@ - package code.snippet +package code.snippet import code.bankconnectors.Connector import net.liftweb.util.Helpers._ diff --git a/src/main/scala/code/tesobe/CashAPI.scala b/src/main/scala/code/tesobe/CashAPI.scala index 80999fe56..1fd178939 100644 --- a/src/main/scala/code/tesobe/CashAPI.scala +++ b/src/main/scala/code/tesobe/CashAPI.scala @@ -1,4 +1,4 @@ - package code.tesobe +package code.tesobe import java.util.Date import code.bankconnectors.Connector diff --git a/src/main/scala/code/tesobe/ImporterAPI.scala b/src/main/scala/code/tesobe/ImporterAPI.scala index 584789ff3..37bfc6e5d 100644 --- a/src/main/scala/code/tesobe/ImporterAPI.scala +++ b/src/main/scala/code/tesobe/ImporterAPI.scala @@ -1,4 +1,4 @@ - package code.tesobe +package code.tesobe import java.util.Date diff --git a/src/main/scala/code/tesobe/TransactionInserter.scala b/src/main/scala/code/tesobe/TransactionInserter.scala index 92344e4c0..391200fce 100644 --- a/src/main/scala/code/tesobe/TransactionInserter.scala +++ b/src/main/scala/code/tesobe/TransactionInserter.scala @@ -1,4 +1,4 @@ - package code.tesobe +package code.tesobe import code.bankconnectors.Connector import code.model.Transaction diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala index 3a05962ac..434c1ff76 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala @@ -1,4 +1,4 @@ - package code.bankaccountcreation +package code.bankaccountcreation import code.api.DefaultConnectorTestSetup import code.api.test.ServerSetup diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala index 74670067c..2f05a9715 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala @@ -1,4 +1,4 @@ - package code.bankaccountcreation +package code.bankaccountcreation import code.api.test.ServerSetup import code.api.{DefaultConnectorTestSetup, DefaultUsers} diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index a99c607f8..8f351b4c4 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -29,7 +29,7 @@ Berlin 13359, Germany Ayoub Benali: ayoub AT tesobe DOT com */ - package code.sandbox +package code.sandbox import java.util.Date diff --git a/src/test/scala/code/tesobe/CashAPITest.scala b/src/test/scala/code/tesobe/CashAPITest.scala index e3293ec79..f6afc9e57 100644 --- a/src/test/scala/code/tesobe/CashAPITest.scala +++ b/src/test/scala/code/tesobe/CashAPITest.scala @@ -1,4 +1,4 @@ - package code.tesobe +package code.tesobe import java.util.{UUID, Date} import net.liftweb.json.Serialization.write diff --git a/src/test/scala/code/tesobe/ImporterTest.scala b/src/test/scala/code/tesobe/ImporterTest.scala index ca7456757..41760f3af 100644 --- a/src/test/scala/code/tesobe/ImporterTest.scala +++ b/src/test/scala/code/tesobe/ImporterTest.scala @@ -1,4 +1,4 @@ - package code.tesobe +package code.tesobe import java.text.SimpleDateFormat import java.util.TimeZone From dd01018523b0711d74bfae49f6e134ee1bb87f38 Mon Sep 17 00:00:00 2001 From: Everett Sochowski Date: Thu, 5 Mar 2015 12:22:51 +0100 Subject: [PATCH 003/702] Add comment --- src/main/scala/code/bankconnectors/Connector.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 6cfbb673b..241d32e10 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -41,6 +41,9 @@ case class OBPOrdering(field: Option[String], order: OBPOrder) extends OBPQueryP trait Connector { + //We have the Connector define its BankAccount implementation here so that it can + //have access to the implementation details (e.g. the ability to set the balance) in + //the implementation of makePaymentImpl type AccountType <: BankAccount //gets a particular bank handled by this connector From f147036a50e3473758669991a643cd8b20aa9909 Mon Sep 17 00:00:00 2001 From: Everett Sochowski Date: Fri, 6 Mar 2015 16:24:31 +0100 Subject: [PATCH 004/702] Update sample props file with tesobe specific settings --- .../resources/props/sample.props.template | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 284d1142b..60d5217f3 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -21,4 +21,28 @@ mail.smtp.port=25 #secret key that allows access to api calls to get info about oauth tokens. You should change this #to your own secret key -BankMockKey=0Msfsofo3px99v0annf09s9j032 \ No newline at end of file +BankMockKey=0Msfsofo3px99v0annf09s9j032 + + +#tesobe specific settings + +#secret key that allows access to the "add cash transactions" api. You should change this to your own secret key +cashApplicationKey=c0cc72b899109dh98dwh + +#secret key that allows access to the "add transactions" api. You should change this to your own secret key +importer_secret=nmoiai2noth839hsioihqohihqop + +#set this to true if you want to have the api to send a message to the hbci project to refresh transactions for an account +messageQueue.updateBankAccountsTransaction=false + +#set this to true if you want to have the api listen for "create account" messages from the hbci project +messageQueue.createBankAccounts=true + +#set this to true if you want to allow users to create sandbox test accounts with a starting balance +allow_sandbox_account_creation=true + +#set this to true if you want to allow the "data import" api call +allow_sandbox_data_import=true + +#secret key that allows access to the "data import" api. You should change this to your own secret key +sandbox_data_import_secret=09ejf09jf09efje09jfe0jw23rssnkndiu733n \ No newline at end of file From a64eeea86253bd2f967a2266345ec82bc12af277 Mon Sep 17 00:00:00 2001 From: Everett Sochowski Date: Mon, 9 Mar 2015 10:44:07 +0100 Subject: [PATCH 005/702] Add rabbitmq props settings --- src/main/resources/props/sample.props.template | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 60d5217f3..8c09ffb8a 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -26,6 +26,12 @@ BankMockKey=0Msfsofo3px99v0annf09s9j032 #tesobe specific settings +#rabbitMQ settings (used to communicate with HBCI project) +connection.host=localhost +connection.user=theusername +connection.password=thepassword + + #secret key that allows access to the "add cash transactions" api. You should change this to your own secret key cashApplicationKey=c0cc72b899109dh98dwh From 080233a61c222b68419c0f6ba40c9ed9126a8140 Mon Sep 17 00:00:00 2001 From: Everett Sochowski Date: Mon, 9 Mar 2015 17:35:04 +0100 Subject: [PATCH 006/702] Add option to change port when running the project via RunWebApp --- src/main/resources/props/sample.props.template | 5 +++++ src/test/scala/RunWebApp.scala | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 284d1142b..c8992f1e8 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -10,6 +10,11 @@ db.url=jdbc:postgresql://localhost:5432/dbname?user=dbusername&password=thepassw # if this is 127.0.0.1 don't use localhost to access it. hostname=http://127.0.0.1:8080 +#this is only useful for running the api locally via RunWebApp +#if you use it, make sure this matches your hostname port! +#if you want to change the port when running via the command line, use "mvn -Djetty.port=8089 jetty:run" instead +dev.port=8089 + #set this to false if you don't want the api payments call to work payments_enabled=true diff --git a/src/test/scala/RunWebApp.scala b/src/test/scala/RunWebApp.scala index 4a04279f5..504c0dc28 100755 --- a/src/test/scala/RunWebApp.scala +++ b/src/test/scala/RunWebApp.scala @@ -29,11 +29,12 @@ Berlin 13359, Germany Ayoub Benali: ayoub AT tesobe DOT com */ +import net.liftweb.util.Props import org.eclipse.jetty.server.Server import org.eclipse.jetty.webapp.WebAppContext object RunWebApp extends App { - val server = new Server(8080) + val server = new Server(Props.getInt("dev.port", 8080)) val context = new WebAppContext() context.setServer(server) From e0321ebc752322bcfff5640178266468e020a606 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 26 Mar 2015 18:59:38 +0100 Subject: [PATCH 007/702] don't fail with 404 if json is invalid (actually return the parsing error and return 400) work with Stefan --- src/main/scala/code/api/OBPRestHelper.scala | 36 ++++++++++++++++++--- src/test/scala/RunWebApp.scala | 3 +- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 8ba1b2448..7cec6ae27 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -79,7 +79,7 @@ trait OBPRestHelper extends RestHelper with Loggable { val VERSION : String def vPlusVersion = "v" + VERSION - def apiPrefix = "obp" / vPlusVersion oPrefix _ + def apiPrefix = ("obp" / vPlusVersion).oPrefix(_) implicit def jsonResponseBoxToJsonReponse(box: Box[JsonResponse]): JsonResponse = { box match { @@ -96,6 +96,18 @@ trait OBPRestHelper extends RestHelper with Loggable { } } + + def failIfBadJSON(r: Req, h: (PartialFunction[Req, Box[User] => Box[JsonResponse]])): Box[User] => Box[JsonResponse] = { + r.json_? match { + case true => + r.json match { + case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"Invalid JSON: $msg")) + case _ => h(r) + } + case false => h(r) + } + } + def failIfBadOauth(fn: (Box[User]) => Box[JsonResponse]) : JsonResponse = { if (isThereAnOAuthHeader) { getUser match { @@ -133,18 +145,32 @@ trait OBPRestHelper extends RestHelper with Loggable { val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = { new PartialFunction[Req, () => Box[LiftResponse]] { def apply(r : Req) = { + //check (in that order): + //if request is correct json + //if request matches PartialFunction cases for each defined url + //if request has correct oauth headers failIfBadOauth { - handler(r) + failIfBadJSON(r, handler) + } + } + def isDefinedAt(r : Req) = { + //if the content-type is json and json parsing failed, simply accept call but then fail in apply() above + //the cases don't match if json failed and don't allow + r.json_? match { + case true => + r.json match { + case Failure(msg, _, _) => true + case _ => handler.isDefinedAt(r) + } + case false => handler.isDefinedAt(r) } } - def isDefinedAt(r : Req) = handler.isDefinedAt(r) } } serve(obpHandler) } - override protected def serve(handler: PartialFunction[Req, () => Box[LiftResponse]]) : Unit= { - + override protected def serve(handler: PartialFunction[Req, () => Box[LiftResponse]]) : Unit = { val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = { new PartialFunction[Req, () => Box[LiftResponse]] { def apply(r : Req) = { diff --git a/src/test/scala/RunWebApp.scala b/src/test/scala/RunWebApp.scala index 4a04279f5..504c0dc28 100755 --- a/src/test/scala/RunWebApp.scala +++ b/src/test/scala/RunWebApp.scala @@ -29,11 +29,12 @@ Berlin 13359, Germany Ayoub Benali: ayoub AT tesobe DOT com */ +import net.liftweb.util.Props import org.eclipse.jetty.server.Server import org.eclipse.jetty.webapp.WebAppContext object RunWebApp extends App { - val server = new Server(8080) + val server = new Server(Props.getInt("dev.port", 8080)) val context = new WebAppContext() context.setServer(server) From d7c2881ae5077a23c4b8575a9ef8e6b4c583b861 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 27 Mar 2015 00:07:39 +0100 Subject: [PATCH 008/702] update dependencies: scala 2.10.5, lift 2.6.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2888e0fe9..9cf8dee29 100644 --- a/pom.xml +++ b/pom.xml @@ -12,8 +12,8 @@ 2011 2.10 - 2.10.4 - 2.6-M4 + 2.10.5 + 2.6.2 UTF-8 ${project.build.sourceEncoding} From 6548e646960378c1913085717fbbbc26444e71cd Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 27 Mar 2015 23:35:45 +0100 Subject: [PATCH 009/702] Adding some comments. Not working yet. --- src/main/scala/code/api/OBPRestHelper.scala | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 7cec6ae27..4e5f43fe2 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -97,9 +97,25 @@ trait OBPRestHelper extends RestHelper with Loggable { } + /* + A method which takes + a Request r + and + a partial function h + which takes + a Request + and + a User + and returns a JsonResponse + and returns a JsonResponse (but what about the User?) + + + */ def failIfBadJSON(r: Req, h: (PartialFunction[Req, Box[User] => Box[JsonResponse]])): Box[User] => Box[JsonResponse] = { + // Check if the content-type is text/json or application/json r.json_? match { case true => + logger.info("Yes we have content-type is json") r.json match { case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"Invalid JSON: $msg")) case _ => h(r) @@ -141,6 +157,14 @@ trait OBPRestHelper extends RestHelper with Loggable { //Give all lists of strings in OBPRestHelpers the oPrefix method implicit def stringListToRichStringList(list : List[String]) : RichStringList = new RichStringList(list) + /* + oauthServe wraps many get calls and probably all calls that post (and put and delete) json data. + Since the URL path matching will fail if there is invalid JsonPost, and this leads to a generic 404 response which is confusing to the developer, + we want to detect invalid json *before* matching on the url so we can fail with a more specific message. + See SandboxApiCalls for an example of JsonPost being used. + The down side is that we might be validating json more than once per request and we're doing work before authentication is completed (possible DOS vector) + */ + def oauthServe(handler : PartialFunction[Req, Box[User] => Box[JsonResponse]]) : Unit = { val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = { new PartialFunction[Req, () => Box[LiftResponse]] { From 24acbab745fe669a84714bcea1df34ad53f38584 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Wed, 1 Apr 2015 18:54:57 +0200 Subject: [PATCH 010/702] allow mvn incremental recompile (using zinc) --- pom.xml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9cf8dee29..404fd0bd3 100644 --- a/pom.xml +++ b/pom.xml @@ -199,12 +199,15 @@ scala-maven-plugin 3.2.0 - ${scala.compiler} ${project.build.sourceEncoding} -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties + ${scala.compiler} + ${scala.version} + incremental + true @@ -212,6 +215,21 @@ compile testCompile + + + -make:transitive + -dependencyfile + ${project.build.directory}/.scala_dependencies + -deprecation + + + + + scala-test-compile + process-test-resources + + testCompile + @@ -325,12 +343,15 @@ scala-maven-plugin 3.2.0 - ${scala.compiler} ${project.build.sourceEncoding} -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties + ${scala.version} + ${scala.compiler} + incremental + true From 75e326963a6d166ec76d7443e80db1b131d82305 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Wed, 1 Apr 2015 19:06:26 +0200 Subject: [PATCH 011/702] fix tests and another deprecation --- src/main/scala/code/model/dataAccess/Account.scala | 4 ++-- src/test/scala/code/tesobe/CashAPITest.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/model/dataAccess/Account.scala b/src/main/scala/code/model/dataAccess/Account.scala index 2429e8600..17ce4a5a7 100644 --- a/src/main/scala/code/model/dataAccess/Account.scala +++ b/src/main/scala/code/model/dataAccess/Account.scala @@ -197,11 +197,11 @@ class HostedBank extends Bank with MongoRecord[HostedBank] with ObjectIdPk[Hoste object national_identifier extends StringField(this, 255) def getAccount(bankAccountId: AccountId) : Box[Account] = { - Account.find((Account.permalink.name -> bankAccountId.value) ~ (Account.bankID.name -> id.is)) ?~ {"account " + bankAccountId +" not found at bank " + permalink} + Account.find((Account.permalink.name -> bankAccountId.value) ~ (Account.bankID.name -> id.get)) ?~ {"account " + bankAccountId +" not found at bank " + permalink} } def isAccount(bankAccountId : AccountId) : Boolean = - Account.count((Account.permalink.name -> bankAccountId.value) ~ (Account.bankID.name -> id.is)) == 1 + Account.count((Account.permalink.name -> bankAccountId.value) ~ (Account.bankID.name -> id.get)) == 1 override def bankId: BankId = BankId(permalink.get) override def shortName: String = alias.get diff --git a/src/test/scala/code/tesobe/CashAPITest.scala b/src/test/scala/code/tesobe/CashAPITest.scala index f6afc9e57..b6e3dfd74 100644 --- a/src/test/scala/code/tesobe/CashAPITest.scala +++ b/src/test/scala/code/tesobe/CashAPITest.scala @@ -35,7 +35,7 @@ class CashAPITest extends ServerSetup with Loggable with DefaultConnectorTestSet } val CashKeyParam = "cashApplicationKey" - val validKey = Props.get(CashKeyParam).get + val validKey = Props.get(CashKeyParam).openOrThrowException("Props key CashKeyParam not found") def fixture() = new { lazy val bank = createBank("test-bank") From 4cc2936644a414bba31ff8a1cc42429a7570db16 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Wed, 1 Apr 2015 18:54:57 +0200 Subject: [PATCH 012/702] allow mvn incremental recompile (using zinc) --- pom.xml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2888e0fe9..6ea5c2247 100644 --- a/pom.xml +++ b/pom.xml @@ -199,12 +199,15 @@ scala-maven-plugin 3.2.0 - ${scala.compiler} ${project.build.sourceEncoding} -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties + ${scala.compiler} + ${scala.version} + incremental + true @@ -212,6 +215,21 @@ compile testCompile + + + -make:transitive + -dependencyfile + ${project.build.directory}/.scala_dependencies + -deprecation + + + + + scala-test-compile + process-test-resources + + testCompile + @@ -325,12 +343,15 @@ scala-maven-plugin 3.2.0 - ${scala.compiler} ${project.build.sourceEncoding} -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties + ${scala.version} + ${scala.compiler} + incremental + true From 5cb7b44ad3b8d3f66d18bfa3a332dc67dc5eed00 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Wed, 1 Apr 2015 20:12:46 +0200 Subject: [PATCH 013/702] fixed rabbitmq case class serial, remove mongodb init call (seems to be unnecessary) --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- .../code/model/dataAccess/BankAccountCreationDispatcher.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 74e0fc8ce..c16a0928e 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -128,7 +128,7 @@ class Boot extends Loggable{ // This sets up MongoDB config - MongoConfig.init + //MongoConfig.init // set up the way to connect to the relational DB we're using if (!DB.jndiJdbcConnAvailable_?) { diff --git a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala index 95604f67c..8b0e0a53e 100644 --- a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala +++ b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala @@ -36,8 +36,8 @@ Berlin 13359, Germany * so that the API create an Bank (if necessary), * the bank account and an owner view. */ -package com.tesobe.model{ - case class CreateBankAccount( +package com.tesobe.model { +@SerialVersionUID(3988687883966746423L) case class CreateBankAccount ( accountOwnerId: String, accountOwnerProvider: String, accountNumber: String, From 3fe3455f858ab865c7b41f7905ab3b322da1f1bb Mon Sep 17 00:00:00 2001 From: florind Date: Wed, 1 Apr 2015 21:32:21 +0200 Subject: [PATCH 014/702] scalac: limit filenames to 130 chars to correctly build in docker --- pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pom.xml b/pom.xml index 6ea5c2247..20f50538c 100644 --- a/pom.xml +++ b/pom.xml @@ -204,6 +204,10 @@ -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties + + -Xmax-classfile-name + 130 + ${scala.compiler} ${scala.version} incremental From 3b9a9220ea2fb6c5f1bfc5a5288ace9e2322f46b Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Wed, 1 Apr 2015 20:12:46 +0200 Subject: [PATCH 015/702] remove mongodb init call (seems to be unnecessary) --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index eed2938ce..435993981 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -126,7 +126,7 @@ class Boot extends Loggable{ // This sets up MongoDB config - MongoConfig.init + //MongoConfig.init // set up the way to connect to the relational DB we're using if (!DB.jndiJdbcConnAvailable_?) { From 500cbf770695d6ab8954aaeeea8d5c40b989d62e Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 2 Apr 2015 01:26:19 +0200 Subject: [PATCH 016/702] add mvn.sh to use increased memory (necessary for incremental compile). use it instead of $ mvn or start zinc. --- mvn.sh | 5 +++++ pom.xml | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100755 mvn.sh diff --git a/mvn.sh b/mvn.sh new file mode 100755 index 000000000..bd15212a3 --- /dev/null +++ b/mvn.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +export MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=256m" + +mvn $1 $2 $3 $4 diff --git a/pom.xml b/pom.xml index 20f50538c..b1cc35cf0 100644 --- a/pom.xml +++ b/pom.xml @@ -162,9 +162,10 @@ 1.0-RC1 ${project.build.directory}/surefire-reports + once . WDF TestSuite.txt - -Drun.mode=test + -Drun.mode=test -XX:MaxPermSize=128m -Xms512m -Xmx512m @@ -201,7 +202,6 @@ ${project.build.sourceEncoding} - -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties @@ -349,7 +349,6 @@ ${project.build.sourceEncoding} - -Xmx1024m -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties ${scala.version} From beb5fb7bc61e5274a3d0da302b5e3c40142b0cfd Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 27 Mar 2015 00:07:39 +0100 Subject: [PATCH 017/702] update dependencies: scala 2.10.5, lift 2.6.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b1cc35cf0..6a7cfd09b 100644 --- a/pom.xml +++ b/pom.xml @@ -12,8 +12,8 @@ 2011 2.10 - 2.10.4 - 2.6-M4 + 2.10.5 + 2.6.2 UTF-8 ${project.build.sourceEncoding} From e023dbd5a50ea9ee21f86a41ec3bd1503c4709dc Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 2 Apr 2015 01:32:25 +0200 Subject: [PATCH 018/702] readme and style --- README.md | 6 +++--- pom.xml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a090ba3e5..f62486782 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Please refer to the [wiki](https://github.com/OpenBankProject/OBP-API/wiki) to s ## STATUS -[V1.2] (https://github.com/OpenBankProject/OBP-API/wiki/REST-API-V1.2) is mostly implemented +[V1.2.1] (https://github.com/OpenBankProject/OBP-API/wiki/REST-API-V1.2.1) is the current stable API. ## LICENSE @@ -35,11 +35,11 @@ The project uses Maven 3 as its build tool. To compile and run jetty, install Maven 3 and execute: -mvn jetty:run +./mvn.sh jetty:run ---- # Databases: -The default datastores used are MongoDB (metadata, transaction cache) and Postgres (user accounts). +The default datastore used is PostgreSQL (user accounts, metadata, transaction cache). diff --git a/pom.xml b/pom.xml index 6a7cfd09b..f4a4cd3a5 100644 --- a/pom.xml +++ b/pom.xml @@ -204,10 +204,10 @@ -DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties - - -Xmax-classfile-name - 130 - + + -Xmax-classfile-name + 130 + ${scala.compiler} ${scala.version} incremental From 62409487d69f1ab63c486072f939aaea9ee58cee Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 2 Apr 2015 01:32:25 +0200 Subject: [PATCH 019/702] more memory --- mvn.sh | 2 +- pom.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mvn.sh b/mvn.sh index bd15212a3..b1af2234a 100755 --- a/mvn.sh +++ b/mvn.sh @@ -1,5 +1,5 @@ #!/bin/sh -export MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=256m" +export MAVEN_OPTS="-Xmx512m -Xms512m -XX:MaxPermSize=256m" mvn $1 $2 $3 $4 diff --git a/pom.xml b/pom.xml index f4a4cd3a5..151391896 100644 --- a/pom.xml +++ b/pom.xml @@ -276,6 +276,7 @@ / 5 + -Xmx512m -Xms512m From dcd1da5f9a3fb465c54363f10c35773b0e2fffe9 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 3 Apr 2015 13:16:05 +0200 Subject: [PATCH 020/702] Removing Facebook, eviscape and polarizeit links, tweaking copyright notice years --- src/main/webapp/templates-hidden/default.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index e438a9aea..82f3eff1c 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -99,18 +99,14 @@ Berlin 13359, Germany - \ No newline at end of file From 9e4f84edf34406247d28ad06108eb32b083da8d7 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Apr 2015 11:11:08 +0200 Subject: [PATCH 021/702] Default new public views to canSeeTransactionDescription=false --- src/main/scala/code/model/dataAccess/view.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/model/dataAccess/view.scala b/src/main/scala/code/model/dataAccess/view.scala index 2d75309b3..9599e46c5 100644 --- a/src/main/scala/code/model/dataAccess/view.scala +++ b/src/main/scala/code/model/dataAccess/view.scala @@ -547,7 +547,7 @@ object ViewImpl extends ViewImpl with LongKeyedMetaMapper[ViewImpl]{ canSeeTransactionThisBankAccount_(true). canSeeTransactionOtherBankAccount_(true). canSeeTransactionMetadata_(true). - canSeeTransactionDescription_(true). + canSeeTransactionDescription_(false). canSeeTransactionAmount_(true). canSeeTransactionType_(true). canSeeTransactionCurrency_(true). From b0c316be5452701695631171f175d4970b4442e4 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Apr 2015 11:12:10 +0200 Subject: [PATCH 022/702] Changing wording for consumer API key registration --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- src/main/webapp/consumer-registration.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index c16a0928e..140895737 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -216,7 +216,7 @@ class Boot extends Loggable{ Menu.i("Home") / "index", Menu.i("Consumer Admin") / "admin" / "consumers" >> Admin.loginFirst >> LocGroup("admin") submenus(Consumer.menus : _*), - Menu("Consumer Registration", "Developers") / "consumer-registration", + Menu("Consumer Registration", "Get API Key") / "consumer-registration", // Menu.i("Metrics") / "metrics", //TODO: allow this page once we can make the account number anonymous in the URL Menu.i("OAuth") / "oauth" / "authorize", //OAuth authorization page OAuthWorkedThanks.menu //OAuth thanks page that will do the redirect diff --git a/src/main/webapp/consumer-registration.html b/src/main/webapp/consumer-registration.html index 8f901b74d..546fe6e55 100644 --- a/src/main/webapp/consumer-registration.html +++ b/src/main/webapp/consumer-registration.html @@ -32,7 +32,7 @@ Berlin 13359, Germany
-

Register to use the Open Bank API with your application:

+

Register your application

@@ -43,7 +43,7 @@ Berlin 13359, Germany - Please complete the information below so we can get an OAuth key and secret. + Please register your application to get your OAuth token and secret: From e9bc3610a4d08298a394f4dde3f83bf33a3a8d35 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Apr 2015 11:12:42 +0200 Subject: [PATCH 023/702] Just comments / english --- src/main/scala/code/api/1_2_1/APIMethods121.scala | 2 +- src/main/scala/code/model/BankingData.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/1_2_1/APIMethods121.scala b/src/main/scala/code/api/1_2_1/APIMethods121.scala index 0c7d5a656..65d438f93 100644 --- a/src/main/scala/code/api/1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/1_2_1/APIMethods121.scala @@ -196,7 +196,7 @@ trait APIMethods121 { for { u <- user ?~ "user not found" account <- BankAccount(bankId, accountId) - views <- account views u + views <- account views u // In other words: views = account.views(u) This calls BankingData.scala BankAccount.views } yield { val viewsJSON = JSONFactory.createViewsJSON(views) successJsonResponse(Extraction.decompose(viewsJSON)) diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index f84de80c2..5fbab86dd 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -340,7 +340,7 @@ trait BankAccount { } final def views(user : User) : Box[List[View]] = { - //check if the user have access to the owner view in this the account + //check if the user has access to the owner view in this the account if(user.ownerAccess(this)) Full(Views.views.vend.views(this)) else From 7e9add0d20b5d7605e631c4aa3188ddb2b53d429 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 3 Apr 2015 13:16:05 +0200 Subject: [PATCH 024/702] Removing Facebook, eviscape and polarizeit links, tweaking copyright notice years --- src/main/webapp/templates-hidden/default.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index e438a9aea..82f3eff1c 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -99,18 +99,14 @@ Berlin 13359, Germany
- \ No newline at end of file From 4f219f91bf3b810bd028444b5045936d626cce2c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Apr 2015 11:11:08 +0200 Subject: [PATCH 025/702] Default new public views to canSeeTransactionDescription=false --- src/main/scala/code/model/dataAccess/view.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/model/dataAccess/view.scala b/src/main/scala/code/model/dataAccess/view.scala index 2d75309b3..9599e46c5 100644 --- a/src/main/scala/code/model/dataAccess/view.scala +++ b/src/main/scala/code/model/dataAccess/view.scala @@ -547,7 +547,7 @@ object ViewImpl extends ViewImpl with LongKeyedMetaMapper[ViewImpl]{ canSeeTransactionThisBankAccount_(true). canSeeTransactionOtherBankAccount_(true). canSeeTransactionMetadata_(true). - canSeeTransactionDescription_(true). + canSeeTransactionDescription_(false). canSeeTransactionAmount_(true). canSeeTransactionType_(true). canSeeTransactionCurrency_(true). From c9364a9573c48948b75538443244b08732041c20 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Apr 2015 11:12:10 +0200 Subject: [PATCH 026/702] Changing wording for consumer API key registration --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- src/main/webapp/consumer-registration.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index eed2938ce..29371ede1 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -202,7 +202,7 @@ class Boot extends Loggable{ Menu.i("Home") / "index", Menu.i("Consumer Admin") / "admin" / "consumers" >> Admin.loginFirst >> LocGroup("admin") submenus(Consumer.menus : _*), - Menu("Consumer Registration", "Developers") / "consumer-registration", + Menu("Consumer Registration", "Get API Key") / "consumer-registration", // Menu.i("Metrics") / "metrics", //TODO: allow this page once we can make the account number anonymous in the URL Menu.i("OAuth") / "oauth" / "authorize", //OAuth authorization page OAuthWorkedThanks.menu //OAuth thanks page that will do the redirect diff --git a/src/main/webapp/consumer-registration.html b/src/main/webapp/consumer-registration.html index 8f901b74d..546fe6e55 100644 --- a/src/main/webapp/consumer-registration.html +++ b/src/main/webapp/consumer-registration.html @@ -32,7 +32,7 @@ Berlin 13359, Germany
-

Register to use the Open Bank API with your application:

+

Register your application

@@ -43,7 +43,7 @@ Berlin 13359, Germany - Please complete the information below so we can get an OAuth key and secret. + Please register your application to get your OAuth token and secret: From a18dd4d97832c72a7bf2c45255f92f14d11ba6bc Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Apr 2015 11:12:42 +0200 Subject: [PATCH 027/702] Just comments / english --- src/main/scala/code/api/1_2_1/APIMethods121.scala | 2 +- src/main/scala/code/model/BankingData.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/1_2_1/APIMethods121.scala b/src/main/scala/code/api/1_2_1/APIMethods121.scala index 0c7d5a656..65d438f93 100644 --- a/src/main/scala/code/api/1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/1_2_1/APIMethods121.scala @@ -196,7 +196,7 @@ trait APIMethods121 { for { u <- user ?~ "user not found" account <- BankAccount(bankId, accountId) - views <- account views u + views <- account views u // In other words: views = account.views(u) This calls BankingData.scala BankAccount.views } yield { val viewsJSON = JSONFactory.createViewsJSON(views) successJsonResponse(Extraction.decompose(viewsJSON)) diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index f84de80c2..5fbab86dd 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -340,7 +340,7 @@ trait BankAccount { } final def views(user : User) : Box[List[View]] = { - //check if the user have access to the owner view in this the account + //check if the user has access to the owner view in this the account if(user.ownerAccess(this)) Full(Views.views.vend.views(this)) else From f7fcb19d62fc4f4f6d54f0fa3e94847f62750736 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 15 Apr 2015 20:20:28 +0200 Subject: [PATCH 028/702] Adding Branches to Sandbox Import - and related tests, traits etc. --- .../code/bankbranches/BankBranches.scala | 20 +- src/main/scala/code/model/BankingData.scala | 9 + .../LocalMappedConnectorDataImport.scala | 24 +++ .../scala/code/sandbox/OBPDataImport.scala | 31 ++- .../code/sandbox/SandboxDataLoadingTest.scala | 179 ++++++++++++++---- 5 files changed, 222 insertions(+), 41 deletions(-) diff --git a/src/main/scala/code/bankbranches/BankBranches.scala b/src/main/scala/code/bankbranches/BankBranches.scala index 5041a8cd1..b042b2980 100644 --- a/src/main/scala/code/bankbranches/BankBranches.scala +++ b/src/main/scala/code/bankbranches/BankBranches.scala @@ -1,7 +1,7 @@ package code.bankbranches import code.bankbranches.BankBranches.{BankBranch, DataLicense, BranchData} -import code.model.BankId +import code.model.{BranchId, BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector @@ -53,6 +53,24 @@ trait BankBranchesProvider { } } + // TODO work in progress. Add singular BranchData + final def getBranch(bank : BankId, branch : BranchId) : Option[BranchData] = { + branchDataLicense(bank) match { + case Some(license) => + Some(BranchData(branchData(bank), license)) + case None => { + logger.info(s"No branch data license found for bank ${bank.value}") + None + } + } + } + + + + + + + protected def branchData(bank : BankId) : List[BankBranch] protected def branchDataLicense(bank : BankId) : Option[DataLicense] } diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 5fbab86dd..1b65a4d82 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -88,6 +88,15 @@ object BankId { def unapply(id : String) = Some(BankId(id)) } +case class BranchId(val value : String) { + override def toString = value +} + +object BranchId { + def unapply(id : String) = Some(BranchId(id)) +} + + trait Bank { def bankId: BankId def shortName : String diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index a55515665..88adcead3 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -3,6 +3,7 @@ package code.sandbox import code.metadata.counterparties.{MappedCounterpartyMetadata} import code.model.dataAccess.{MappedBankAccount, MappedBank} import code.model.{MappedTransaction, AccountId, BankId} +import code.bankbranches.{MappedBankBranch, MappedDataLicense} import code.util.Helper.convertToSmallestCurrencyUnits import net.liftweb.common.{Full, Failure, Box} import net.liftweb.mapper.Mapper @@ -18,6 +19,8 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls type AccountType = MappedBankAccount type MetadataType = MappedCounterpartyMetadata type TransactionType = MappedTransaction + type BankBranchType = MappedBankBranch + type DataLicenseType = MappedDataLicense protected def createSaveableBanks(data : List[SandboxBankImport]) : Box[List[Saveable[BankType]]] = { val mappedBanks = data.map(bank => { @@ -38,6 +41,27 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls } } + protected def createSaveableBranches(data : List[SandboxBankBranchImport]) : Box[List[Saveable[BankBranchType]]] = { + val mappedBankBranches = data.map(bankBranch => { + MappedBankBranch.create + .mBranchId(bankBranch.id) + .mBankId(bankBranch.bank) + .mName(bankBranch.name) + // TODO add the other fields + }) + + val validationErrors = mappedBankBranches.flatMap(_.validate) + + if(validationErrors.nonEmpty) { + Failure(s"Errors: ${validationErrors.map(_.msg)}") + } else { + Full(mappedBankBranches.map(MappedSaveable(_))) + } + } + + + + protected def createSaveableAccount(acc : SandboxAccountImport, banks : List[BankType]) : Box[Saveable[AccountType]] = { val mappedAccount = for { diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index e0555e700..c771ee0f0 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -377,6 +377,34 @@ case class SandboxBankImport( logo : String, website : String) +case class SandboxBankBranchImport( + id : String, + bank: String, + name : String, + address : SandboxAddressImport, + location : SandboxLocationImport) + +case class SandboxDataLicenseImport( + id : String, + short_name : String, + full_name : String, + logo : String, + website : String) + +case class SandboxAddressImport( + line_1 : String, + line_2 : String, + line_3 : String, + line_4 : String, + line_5 : String, + post_code : String, + country_code: String) + +case class SandboxLocationImport( + latitude : Double, + longitude : Double +) + case class SandboxUserImport( email : String, password : String, @@ -423,4 +451,5 @@ case class SandboxDataImport( banks : List[SandboxBankImport], users : List[SandboxUserImport], accounts : List[SandboxAccountImport], - transactions : List[SandboxTransactionImport]) \ No newline at end of file + transactions : List[SandboxTransactionImport], + branches: List[SandboxBankBranchImport]) \ No newline at end of file diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 8f351b4c4..5870de56b 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -37,8 +37,9 @@ import bootstrap.liftweb.ToSchemify import code.TestServer import code.api.test.{SendServerRequests, APIResponse} import code.api.v1_2_1.APIMethods121 +import code.bankbranches.BankBranches import code.model.dataAccess._ -import code.model.{TransactionId, AccountId, BankId} +import code.model.{BranchId, TransactionId, AccountId, BankId} import code.users.Users import code.views.Views import dispatch._ @@ -72,7 +73,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul override def beforeEach() = { - //drop database tables after the tests + //drop database tables after (before?) the tests MongoDB.getDb(DefaultMongoIdentifier).foreach(_.dropDatabase()) ToSchemify.models.foreach(_.bulkDelete_!!()) } @@ -82,12 +83,13 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul xs.mkString("[", ",", "]") } - def createImportJson(banks: List[JValue], users: List[JValue], accounts : List[JValue], transactions : List[JValue]) : String = { + def createImportJson(banks: List[JValue], users: List[JValue], accounts : List[JValue], transactions : List[JValue], branches : List[JValue]) : String = { val json = ("banks" -> banks) ~ ("users" -> users) ~ ("accounts" -> accounts) ~ - ("transactions" -> transactions) + ("transactions" -> transactions) ~ + ("branches" -> branches) compact(render(json)) } @@ -120,6 +122,34 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul foundBank.websiteUrl should equal(bank.website) } + + /* + WORK IN PROGRESS TODO: Complete! + + */ + def verifyBranchCreated(branch : SandboxBankBranchImport) = { + val branchId = BranchId(branch.id) + + + + +// val foundBranchBox = Connector.connector.vend.getBank(branchId) +// +// +// BankBranches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) +// +// foundBranchBox.isDefined should equal(true) +// +// val foundBranch = foundBranchBox.get + +// +// foundBranch.bankId should equal(branchId) +// foundBranch. should equal(branch.short_name) +// foundBranch.fullName should equal(branch.full_name) +// foundBranch.logoUrl should equal(branch.logo) +// foundBranch.websiteUrl should equal(branch.website) + } + def verifyUserCreated(user : SandboxUserImport) = { val foundUserBox = Users.users.vend.getUserByProviderId(defaultProvider, user.email) foundUserBox.isDefined should equal(true) @@ -265,6 +295,19 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardBanks = bank1 :: bank2 :: Nil + + val standardAddress1 = SandboxAddressImport(line_1 = "5 Some Street", line_2 = "Rosy Place", line_3 = "Sunny Village", + line_4 = "Out There", line_5 = "Derbyshire", post_code = "WHY RU4", country_code = "UK") + + val standardLocation1 = SandboxLocationImport(52.556198, 13.384099) + + + val branch1AtBank1 = SandboxBankBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1) + val branch2AtBank1 = SandboxBankBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1) + + val standardBranches = branch1AtBank1 :: branch2AtBank1 :: Nil + + val user1 = SandboxUserImport(email = "user1@example.com", password = "qwerty", display_name = "User 1") val user2 = SandboxUserImport(email = "user2@example.com", password = "qwerty", display_name = "User 2") @@ -363,8 +406,10 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val users = standardUsers val accounts = standardAccounts val transactions = anotherTransaction :: blankCounterpartyNameTransaction :: blankCounterpartyAccountNumberTransaction :: standardTransactions + val branches = standardBranches - val importJson = SandboxDataImport(banks, users, accounts, transactions) + + val importJson = SandboxDataImport(banks, users, accounts, transactions, branches) val response = postImportJson(write(importJson)) response.code should equal(SUCCESS) @@ -376,7 +421,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul } it should "not allow data to be imported without a secret token" in { - val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions) + val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches) val response = postImportJson(write(importJson), None) response.code should equal(403) @@ -386,7 +431,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul } it should "not allow data to be imported with an invalid secret token" in { - val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions) + val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches) val badToken = "12345" badToken should not equal(theImportToken) val response = postImportJson(write(importJson), Some(badToken)) @@ -407,7 +452,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankWithoutId = removeIdField(bank1Json) def getResponse(bankJson : JValue) = { - val json = createImportJson(List(bankJson), Nil, Nil, Nil) + val json = createImportJson(List(bankJson), Nil, Nil, Nil, Nil) postImportJson(json) } @@ -456,7 +501,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankWithSameId = addIdField(baseOtherBank, bank1.id) def getResponse(bankJsons : List[JValue]) = { - val json = createImportJson(bankJsons, Nil, Nil, Nil) + val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -474,7 +519,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified bank already exists" in { def getResponse(bankJsons : List[JValue]) = { - val json = createImportJson(bankJsons, Nil, Nil, Nil) + val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -495,7 +540,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "require users to have valid emails" in { def getResponse(userJson : JValue) = { - val json = createImportJson(Nil, List(userJson), Nil, Nil) + val json = createImportJson(Nil, List(userJson), Nil, Nil, Nil) postImportJson(json) } @@ -541,7 +586,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "not allow multiple users with the same email" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -587,7 +632,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified user already exists" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -607,7 +652,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a user's password is missing or empty" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -630,7 +675,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "set user passwords properly" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -650,7 +695,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -677,7 +722,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -706,7 +751,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } val account1AtBank1Json = Extraction.decompose(account1AtBank1) @@ -731,7 +776,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -748,7 +793,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -769,7 +814,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks def getResponse(accountJsons : List[JValue]) = { - val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil) + val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil) postImportJson(json) } @@ -797,7 +842,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks def getResponse(accountJsons : List[JValue]) = { - val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil) + val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil) postImportJson(json) } @@ -826,7 +871,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -857,7 +902,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -898,7 +943,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified transaction already exists" in { def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -925,7 +970,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -980,7 +1025,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1008,7 +1053,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1036,7 +1081,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1063,7 +1108,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1090,7 +1135,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1124,7 +1169,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1157,7 +1202,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1193,7 +1238,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1227,7 +1272,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1271,7 +1316,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1316,7 +1361,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1369,7 +1414,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1421,7 +1466,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1448,4 +1493,60 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul t1.otherAccount.id should equal(t2.otherAccount.id) } + + ////// + + it should "require branches to have non-empty ids" in { + + + val (banks) = (standardBanks) + + val bankId = BankId(bank1.id) + + BankBranches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) + + + val branch1Json = Extraction.decompose(branch1AtBank1) + val branchWithoutId = removeIdField(branch1Json) + + def getResponse(branchJson : JValue) = { + val json = createImportJson(banks.map(Extraction.decompose), Nil, Nil, Nil, List(branchJson)) + postImportJson(json) + } + + getResponse(branchWithoutId).code should equal(FAILED) + + getResponse(branch1Json).code should equal(SUCCESS) + + +// +// //no banks should have been created +// Connector.connector.vend.getBanks.size should equal(0) +// +// val bankWithEmptyId = addIdField(bankWithoutId, "") +// getResponse(bankWithEmptyId).code should equal(FAILED) +// +// //no banks should have been created +// Connector.connector.vend.getBanks.size should equal(0) +// +// //Check that the same json becomes valid when a non-empty id is added +// val validId = "foo" +// val bankWithValidId = addIdField(bankWithoutId, validId) +// val response = getResponse(bankWithValidId) +// response.code should equal(SUCCESS) +// +// //Check the bank was created +// val banks = Connector.connector.vend.getBanks +// banks.size should equal(1) +// val createdBank = banks(0) +// +// createdBank.bankId should equal(BankId(validId)) +// createdBank.shortName should equal(bank1.short_name) +// createdBank.fullName should equal(bank1.full_name) +// createdBank.logoUrl should equal(bank1.logo) +// createdBank.websiteUrl should equal(bank1.website) + } + + + } From a5e74d91fc111c9213c337d6fedb940f86e340cb Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 16 Apr 2015 01:21:45 +0200 Subject: [PATCH 029/702] Renaming BankBranches to Branches --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 ++-- .../scala/code/api/v1_4_0/APIMethods140.scala | 4 ++-- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 6 ++--- .../{BankBranches.scala => Branches.scala} | 10 ++++---- ...der.scala => MappedBranchesProvider.scala} | 14 +++++------ .../LocalMappedConnectorDataImport.scala | 8 +++---- ...kBranchesTest.scala => BranchesTest.scala} | 18 +++++++------- ...scala => MappedBranchesProviderTest.scala} | 24 +++++++++---------- .../code/sandbox/SandboxDataLoadingTest.scala | 4 ++-- 9 files changed, 46 insertions(+), 46 deletions(-) rename src/main/scala/code/bankbranches/{BankBranches.scala => Branches.scala} (85%) rename src/main/scala/code/bankbranches/{MappedBankBranchesProvider.scala => MappedBranchesProvider.scala} (78%) rename src/test/scala/code/api/v1_4_0/{BankBranchesTest.scala => BranchesTest.scala} (84%) rename src/test/scala/code/bankbranches/{MappedBankBranchesProviderTest.scala => MappedBranchesProviderTest.scala} (78%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 140895737..ba0c8f049 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -39,7 +39,7 @@ import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric -import code.bankbranches.{MappedBankBranch, MappedDataLicense} +import code.bankbranches.{MappedBranch, MappedDataLicense} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} import code.tesobe.{ImporterAPI, CashAccountAPI} import net.liftweb._ @@ -337,5 +337,5 @@ object ToSchemify { MappedTransactionImage, MappedWhereTag, MappedCounterpartyMetadata, MappedCounterpartyWhereTag, MappedBank, MappedBankAccount, MappedTransaction, MappedMetric, MappedCustomerInfo, MappedCustomerMessage, - MappedBankBranch, MappedDataLicense) + MappedBranch, MappedDataLicense) } diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 2d2e8b906..f21dc3150 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -2,7 +2,7 @@ package code.api.v1_4_0 import code.api.APIFailure import code.api.v1_4_0.JSONFactory1_4_0.AddCustomerMessageJson -import code.bankbranches.BankBranches +import code.bankbranches.Branches import code.customerinfo.{CustomerMessages, CustomerInfo} import code.model.{BankId, User} import net.liftweb.common.Box @@ -71,7 +71,7 @@ trait APIMethods140 { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { for { - branches <- Box(BankBranches.bankBranchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available", 404) + branches <- Box(Branches.bankBranchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available", 404) } yield { val json = JSONFactory1_4_0.createBranchesJson(branches) successJsonResponse(Extraction.decompose(json)) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 810369c9e..ce11d9e54 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -2,8 +2,8 @@ package code.api.v1_4_0 import java.util.Date -import code.bankbranches.BankBranches -import code.bankbranches.BankBranches.{BankBranch, DataLicense, BranchData} +import code.bankbranches.Branches +import code.bankbranches.Branches.{BankBranch, DataLicense, BranchData} import code.customerinfo.{CustomerMessage, CustomerInfo} object JSONFactory1_4_0 { @@ -48,7 +48,7 @@ object JSONFactory1_4_0 { DataLicenseJson(dataLicense.name, dataLicense.url) } - def createAddressJson(address : BankBranches.Address) : AddressJson = { + def createAddressJson(address : Branches.Address) : AddressJson = { AddressJson(address.line1, address.line2, address.line3, address.line4, address.line5, address.postCode, address.countryCode) } diff --git a/src/main/scala/code/bankbranches/BankBranches.scala b/src/main/scala/code/bankbranches/Branches.scala similarity index 85% rename from src/main/scala/code/bankbranches/BankBranches.scala rename to src/main/scala/code/bankbranches/Branches.scala index b042b2980..03e9f2699 100644 --- a/src/main/scala/code/bankbranches/BankBranches.scala +++ b/src/main/scala/code/bankbranches/Branches.scala @@ -1,13 +1,13 @@ package code.bankbranches -import code.bankbranches.BankBranches.{BankBranch, DataLicense, BranchData} +import code.bankbranches.Branches.{BankBranch, DataLicense, BranchData} import code.model.{BranchId, BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector -object BankBranches extends SimpleInjector { +object Branches extends SimpleInjector { - case class BankBranchId(value : String) + case class BranchId(value : String) case class BranchData(branches : List[BankBranch], license : DataLicense) trait DataLicense { @@ -16,7 +16,7 @@ object BankBranches extends SimpleInjector { } trait BankBranch { - def branchId : BankBranchId + def branchId : BranchId def name : String def address : Address } @@ -34,7 +34,7 @@ object BankBranches extends SimpleInjector { val bankBranchesProvider = new Inject(buildOne _) {} - def buildOne: BankBranchesProvider = MappedBankBranchesProvider + def buildOne: BankBranchesProvider = MappedBranchesProvider } diff --git a/src/main/scala/code/bankbranches/MappedBankBranchesProvider.scala b/src/main/scala/code/bankbranches/MappedBranchesProvider.scala similarity index 78% rename from src/main/scala/code/bankbranches/MappedBankBranchesProvider.scala rename to src/main/scala/code/bankbranches/MappedBranchesProvider.scala index 7af640424..02be8f2f2 100644 --- a/src/main/scala/code/bankbranches/MappedBankBranchesProvider.scala +++ b/src/main/scala/code/bankbranches/MappedBranchesProvider.scala @@ -1,21 +1,21 @@ package code.bankbranches -import code.bankbranches.BankBranches.{DataLicense, BankBranchId, Address, BankBranch} +import code.bankbranches.Branches.{DataLicense, BranchId, Address, BankBranch} import code.model.BankId import code.util.DefaultStringField import net.liftweb.mapper._ -object MappedBankBranchesProvider extends BankBranchesProvider { +object MappedBranchesProvider extends BankBranchesProvider { override protected def branchData(bank: BankId): List[BankBranch] = - MappedBankBranch.findAll(By(MappedBankBranch.mBankId, bank.value)) + MappedBranch.findAll(By(MappedBranch.mBankId, bank.value)) override protected def branchDataLicense(bank: BankId): Option[DataLicense] = MappedDataLicense.find(By(MappedDataLicense.mBankId, bank.value)) } -class MappedBankBranch extends BankBranch with LongKeyedMapper[MappedBankBranch] with IdPK { +class MappedBranch extends BankBranch with LongKeyedMapper[MappedBranch] with IdPK { - override def getSingleton = MappedBankBranch + override def getSingleton = MappedBranch object mBankId extends DefaultStringField(this) object mName extends DefaultStringField(this) @@ -32,7 +32,7 @@ class MappedBankBranch extends BankBranch with LongKeyedMapper[MappedBankBranch] object mPostCode extends DefaultStringField(this) - override def branchId: BankBranchId = BankBranchId(mBranchId.get) + override def branchId: BranchId = BranchId(mBranchId.get) override def name: String = mName.get override def address: Address = new Address { @@ -46,7 +46,7 @@ class MappedBankBranch extends BankBranch with LongKeyedMapper[MappedBankBranch] } } -object MappedBankBranch extends MappedBankBranch with LongKeyedMetaMapper[MappedBankBranch] { +object MappedBranch extends MappedBranch with LongKeyedMetaMapper[MappedBranch] { override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes } diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 88adcead3..ae4fb3a5e 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -3,7 +3,7 @@ package code.sandbox import code.metadata.counterparties.{MappedCounterpartyMetadata} import code.model.dataAccess.{MappedBankAccount, MappedBank} import code.model.{MappedTransaction, AccountId, BankId} -import code.bankbranches.{MappedBankBranch, MappedDataLicense} +import code.bankbranches.{MappedBranch, MappedDataLicense} import code.util.Helper.convertToSmallestCurrencyUnits import net.liftweb.common.{Full, Failure, Box} import net.liftweb.mapper.Mapper @@ -19,7 +19,7 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls type AccountType = MappedBankAccount type MetadataType = MappedCounterpartyMetadata type TransactionType = MappedTransaction - type BankBranchType = MappedBankBranch + type BranchType = MappedBranch type DataLicenseType = MappedDataLicense protected def createSaveableBanks(data : List[SandboxBankImport]) : Box[List[Saveable[BankType]]] = { @@ -41,9 +41,9 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls } } - protected def createSaveableBranches(data : List[SandboxBankBranchImport]) : Box[List[Saveable[BankBranchType]]] = { + protected def createSaveableBranches(data : List[SandboxBankBranchImport]) : Box[List[Saveable[BranchType]]] = { val mappedBankBranches = data.map(bankBranch => { - MappedBankBranch.create + MappedBranch.create .mBranchId(bankBranch.id) .mBankId(bankBranch.bank) .mName(bankBranch.name) diff --git a/src/test/scala/code/api/v1_4_0/BankBranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala similarity index 84% rename from src/test/scala/code/api/v1_4_0/BankBranchesTest.scala rename to src/test/scala/code/api/v1_4_0/BranchesTest.scala index bf08db450..f88649167 100644 --- a/src/test/scala/code/api/v1_4_0/BankBranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -2,24 +2,24 @@ package code.api.v1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchDataJson} import dispatch._ -import code.bankbranches.BankBranches.{Address, BankBranchId, BankBranch, DataLicense} -import code.bankbranches.{BankBranches, BankBranchesProvider} +import code.bankbranches.Branches.{Address, BranchId, BankBranch, DataLicense} +import code.bankbranches.{Branches, BankBranchesProvider} import code.model.BankId -class BankBranchesTest extends V140ServerSetup { +class BranchesTest extends V140ServerSetup { val BankWithLicense = BankId("bank-with-license") val BankWithoutLicense = BankId("bank-without-license") - case class BankBranchImpl(branchId : BankBranchId, name : String, address : Address) extends BankBranch + case class BankBranchImpl(branchId : BranchId, name : String, address : Address) extends BankBranch case class AddressImpl(line1 : String, line2 : String, line3 : String, line4 : String, line5 : String, postCode : String, countryCode : String) extends Address val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") val fakeAddress2 = fakeAddress1.copy(line1 = "00000") - val fakeBranch1 = BankBranchImpl(BankBranchId("branch1"), "Branch 1", fakeAddress1) - val fakeBranch2 = BankBranchImpl(BankBranchId("branch2"), "Branch 2", fakeAddress2) + val fakeBranch1 = BankBranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1) + val fakeBranch2 = BankBranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2) val fakeLicense = new DataLicense { override def name: String = "sample-license" @@ -45,7 +45,7 @@ class BankBranchesTest extends V140ServerSetup { def verifySameData(branch: BankBranch, branchJson : BranchJson) = { branch.name should equal (branchJson.name) - branch.branchId should equal(BankBranchId(branchJson.id)) + branch.branchId should equal(BranchId(branchJson.id)) branch.address.line1 should equal(branchJson.address.line_1) branch.address.line2 should equal(branchJson.address.line_2) branch.address.line3 should equal(branchJson.address.line_3) @@ -58,13 +58,13 @@ class BankBranchesTest extends V140ServerSetup { override def beforeAll() { super.beforeAll() //use the mock connector - BankBranches.bankBranchesProvider.default.set(mockConnector) + Branches.bankBranchesProvider.default.set(mockConnector) } override def afterAll() { super.afterAll() //reset the default connector - BankBranches.bankBranchesProvider.default.set(BankBranches.buildOne) + Branches.bankBranchesProvider.default.set(Branches.buildOne) } feature("Getting bank branches") { diff --git a/src/test/scala/code/bankbranches/MappedBankBranchesProviderTest.scala b/src/test/scala/code/bankbranches/MappedBranchesProviderTest.scala similarity index 78% rename from src/test/scala/code/bankbranches/MappedBankBranchesProviderTest.scala rename to src/test/scala/code/bankbranches/MappedBranchesProviderTest.scala index 7b4be1455..c6fbc821c 100644 --- a/src/test/scala/code/bankbranches/MappedBankBranchesProviderTest.scala +++ b/src/test/scala/code/bankbranches/MappedBranchesProviderTest.scala @@ -4,10 +4,10 @@ import code.api.test.ServerSetup import code.model.BankId import net.liftweb.mapper.By -class MappedBankBranchesProviderTest extends ServerSetup { +class MappedBranchesProviderTest extends ServerSetup { private def delete(): Unit = { - MappedBankBranch.bulkDelete_!!() + MappedBranch.bulkDelete_!!() MappedDataLicense.bulkDelete_!!() } @@ -31,7 +31,7 @@ class MappedBankBranchesProviderTest extends ServerSetup { .mName("some-license") .mUrl("http://www.example.com/license").saveMe() - val unlicensedBranch = MappedBankBranch.create + val unlicensedBranch = MappedBranch.create .mBankId(bankIdWithNoLicense) .mName("unlicensed") .mBranchId("unlicensed") @@ -43,7 +43,7 @@ class MappedBankBranchesProviderTest extends ServerSetup { .mLine4("d4") .mLine5("e4").saveMe() - val branch1 = MappedBankBranch.create + val branch1 = MappedBranch.create .mBankId(bankIdWithLicenseAndData) .mName("branch 1") .mBranchId("branch1") @@ -55,7 +55,7 @@ class MappedBankBranchesProviderTest extends ServerSetup { .mLine4("d") .mLine5("e").saveMe() - val branch2 = MappedBankBranch.create + val branch2 = MappedBranch.create .mBankId(bankIdWithLicenseAndData) .mName("branch 2") .mBranchId("branch2") @@ -68,7 +68,7 @@ class MappedBankBranchesProviderTest extends ServerSetup { .mLine5("e2").saveMe() } - feature("MappedBankBranchesProvider") { + feature("MappedBranchesProvider") { scenario("We try to get branch data for a bank which does not have a data license set") { val fixture = defaultSetup() @@ -77,10 +77,10 @@ class MappedBankBranchesProviderTest extends ServerSetup { MappedDataLicense.count(By(MappedDataLicense.mBankId, fixture.bankIdWithNoLicense)) should equal(0) And("The bank in question has branches") - MappedBankBranch.find(By(MappedBankBranch.mBankId, fixture.bankIdWithNoLicense)).isDefined should equal(true) + MappedBranch.find(By(MappedBranch.mBankId, fixture.bankIdWithNoLicense)).isDefined should equal(true) When("We try to get the branch data for that bank") - val branchData = MappedBankBranchesProvider.getBranches(BankId(fixture.bankIdWithNoLicense)) + val branchData = MappedBranchesProvider.getBranches(BankId(fixture.bankIdWithNoLicense)) Then("We should get an empty option") branchData should equal(None) @@ -92,10 +92,10 @@ class MappedBankBranchesProviderTest extends ServerSetup { val expectedBranches = Set(fixture.branch1, fixture.branch2) Given("We have a data license and branches for a bank") MappedDataLicense.count(By(MappedDataLicense.mBankId, fixture.bankIdWithLicenseAndData)) should equal(1) - MappedBankBranch.findAll(By(MappedBankBranch.mBankId, fixture.bankIdWithLicenseAndData)).toSet should equal(expectedBranches) + MappedBranch.findAll(By(MappedBranch.mBankId, fixture.bankIdWithLicenseAndData)).toSet should equal(expectedBranches) When("We try to get the branch data for that bank") - val branchDataOpt = MappedBankBranchesProvider.getBranches(BankId(fixture.bankIdWithLicenseAndData)) + val branchDataOpt = MappedBranchesProvider.getBranches(BankId(fixture.bankIdWithLicenseAndData)) Then("We should get back the data license and the branches") branchDataOpt.isDefined should equal(true) @@ -115,10 +115,10 @@ class MappedBankBranchesProviderTest extends ServerSetup { .mName("some-license") .mUrl("http://www.example.com/license").saveMe() - MappedBankBranch.find(By(MappedBankBranch.mBankId, bankWithNoBranches)).isDefined should equal(false) + MappedBranch.find(By(MappedBranch.mBankId, bankWithNoBranches)).isDefined should equal(false) When("We try to get the branch data for that bank") - val branchDataOpt = MappedBankBranchesProvider.getBranches(BankId(bankWithNoBranches)) + val branchDataOpt = MappedBranchesProvider.getBranches(BankId(bankWithNoBranches)) Then("We should get back the data license, and a list branches of size 0") branchDataOpt.isDefined should equal(true) diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 5870de56b..91acf61eb 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -37,7 +37,7 @@ import bootstrap.liftweb.ToSchemify import code.TestServer import code.api.test.{SendServerRequests, APIResponse} import code.api.v1_2_1.APIMethods121 -import code.bankbranches.BankBranches +import code.bankbranches.Branches import code.model.dataAccess._ import code.model.{BranchId, TransactionId, AccountId, BankId} import code.users.Users @@ -1503,7 +1503,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankId = BankId(bank1.id) - BankBranches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) + Branches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) val branch1Json = Extraction.decompose(branch1AtBank1) From 4341a3fda6cc285c86a5377c393ca66c0bda175a Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 16 Apr 2015 01:25:10 +0200 Subject: [PATCH 030/702] Rename BankBranches to Branches Part II --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 2 +- src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala | 4 ++-- src/main/scala/code/{bankbranches => branches}/Branches.scala | 4 ++-- .../{bankbranches => branches}/MappedBranchesProvider.scala | 4 ++-- .../scala/code/sandbox/LocalMappedConnectorDataImport.scala | 2 +- src/test/scala/code/api/v1_4_0/BranchesTest.scala | 4 ++-- .../MappedBranchesProviderTest.scala | 2 +- src/test/scala/code/sandbox/SandboxDataLoadingTest.scala | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) rename src/main/scala/code/{bankbranches => branches}/Branches.scala (94%) rename src/main/scala/code/{bankbranches => branches}/MappedBranchesProvider.scala (95%) rename src/test/scala/code/{bankbranches => branches}/MappedBranchesProviderTest.scala (99%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index ba0c8f049..66f0cf2e8 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -39,7 +39,7 @@ import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric -import code.bankbranches.{MappedBranch, MappedDataLicense} +import code.branches.{MappedBranch, MappedDataLicense} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} import code.tesobe.{ImporterAPI, CashAccountAPI} import net.liftweb._ diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index f21dc3150..385bb7cd2 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -2,7 +2,7 @@ package code.api.v1_4_0 import code.api.APIFailure import code.api.v1_4_0.JSONFactory1_4_0.AddCustomerMessageJson -import code.bankbranches.Branches +import code.branches.Branches import code.customerinfo.{CustomerMessages, CustomerInfo} import code.model.{BankId, User} import net.liftweb.common.Box diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index ce11d9e54..2a73bf4a3 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -2,8 +2,8 @@ package code.api.v1_4_0 import java.util.Date -import code.bankbranches.Branches -import code.bankbranches.Branches.{BankBranch, DataLicense, BranchData} +import code.branches.Branches +import code.branches.Branches.{BankBranch, DataLicense, BranchData} import code.customerinfo.{CustomerMessage, CustomerInfo} object JSONFactory1_4_0 { diff --git a/src/main/scala/code/bankbranches/Branches.scala b/src/main/scala/code/branches/Branches.scala similarity index 94% rename from src/main/scala/code/bankbranches/Branches.scala rename to src/main/scala/code/branches/Branches.scala index 03e9f2699..523c5feec 100644 --- a/src/main/scala/code/bankbranches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -1,6 +1,6 @@ -package code.bankbranches +package code.branches -import code.bankbranches.Branches.{BankBranch, DataLicense, BranchData} +import code.branches.Branches.{BankBranch, DataLicense, BranchData} import code.model.{BranchId, BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector diff --git a/src/main/scala/code/bankbranches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala similarity index 95% rename from src/main/scala/code/bankbranches/MappedBranchesProvider.scala rename to src/main/scala/code/branches/MappedBranchesProvider.scala index 02be8f2f2..f048de413 100644 --- a/src/main/scala/code/bankbranches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -1,6 +1,6 @@ -package code.bankbranches +package code.branches -import code.bankbranches.Branches.{DataLicense, BranchId, Address, BankBranch} +import code.branches.Branches.{DataLicense, BranchId, Address, BankBranch} import code.model.BankId import code.util.DefaultStringField import net.liftweb.mapper._ diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index ae4fb3a5e..12a5cdc3d 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -3,7 +3,7 @@ package code.sandbox import code.metadata.counterparties.{MappedCounterpartyMetadata} import code.model.dataAccess.{MappedBankAccount, MappedBank} import code.model.{MappedTransaction, AccountId, BankId} -import code.bankbranches.{MappedBranch, MappedDataLicense} +import code.branches.{MappedBranch, MappedDataLicense} import code.util.Helper.convertToSmallestCurrencyUnits import net.liftweb.common.{Full, Failure, Box} import net.liftweb.mapper.Mapper diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index f88649167..da4647c01 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -2,8 +2,8 @@ package code.api.v1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchDataJson} import dispatch._ -import code.bankbranches.Branches.{Address, BranchId, BankBranch, DataLicense} -import code.bankbranches.{Branches, BankBranchesProvider} +import code.branches.Branches.{Address, BranchId, BankBranch, DataLicense} +import code.branches.{Branches, BankBranchesProvider} import code.model.BankId class BranchesTest extends V140ServerSetup { diff --git a/src/test/scala/code/bankbranches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala similarity index 99% rename from src/test/scala/code/bankbranches/MappedBranchesProviderTest.scala rename to src/test/scala/code/branches/MappedBranchesProviderTest.scala index c6fbc821c..bbc9f0d71 100644 --- a/src/test/scala/code/bankbranches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -1,4 +1,4 @@ -package code.bankbranches +package code.branches import code.api.test.ServerSetup import code.model.BankId diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 91acf61eb..f121c79b0 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -37,7 +37,7 @@ import bootstrap.liftweb.ToSchemify import code.TestServer import code.api.test.{SendServerRequests, APIResponse} import code.api.v1_2_1.APIMethods121 -import code.bankbranches.Branches +import code.branches.Branches import code.model.dataAccess._ import code.model.{BranchId, TransactionId, AccountId, BankId} import code.users.Users From cffb0591ae7c0610800387408b0dbb72abb20e76 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 16 Apr 2015 01:36:42 +0200 Subject: [PATCH 031/702] Rename BankBranch to Branch part III --- .../scala/code/api/v1_4_0/APIMethods140.scala | 2 +- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 6 +++--- src/main/scala/code/branches/Branches.scala | 16 +++++++-------- .../branches/MappedBranchesProvider.scala | 8 ++++---- .../LocalMappedConnectorDataImport.scala | 14 ++++++------- .../scala/code/sandbox/OBPDataImport.scala | 4 ++-- .../scala/code/api/v1_4_0/BranchesTest.scala | 20 +++++++++---------- .../code/sandbox/SandboxDataLoadingTest.scala | 8 ++++---- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 385bb7cd2..7dff0a043 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -71,7 +71,7 @@ trait APIMethods140 { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { for { - branches <- Box(Branches.bankBranchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available", 404) + branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available", 404) } yield { val json = JSONFactory1_4_0.createBranchesJson(branches) successJsonResponse(Extraction.decompose(json)) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 2a73bf4a3..5404fdcf0 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -3,7 +3,7 @@ package code.api.v1_4_0 import java.util.Date import code.branches.Branches -import code.branches.Branches.{BankBranch, DataLicense, BranchData} +import code.branches.Branches.{Branch, DataLicense, BranchData} import code.customerinfo.{CustomerMessage, CustomerInfo} object JSONFactory1_4_0 { @@ -52,8 +52,8 @@ object JSONFactory1_4_0 { AddressJson(address.line1, address.line2, address.line3, address.line4, address.line5, address.postCode, address.countryCode) } - def createBranchJson(bankBranch: BankBranch) : BranchJson = { - BranchJson(bankBranch.branchId.value, bankBranch.name, createAddressJson(bankBranch.address)) + def createBranchJson(branch: Branch) : BranchJson = { + BranchJson(branch.branchId.value, branch.name, createAddressJson(branch.address)) } def createBranchesJson(branchData : BranchData) : BranchDataJson = { diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 523c5feec..53136e870 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -1,6 +1,6 @@ package code.branches -import code.branches.Branches.{BankBranch, DataLicense, BranchData} +import code.branches.Branches.{Branch, DataLicense, BranchData} import code.model.{BranchId, BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector @@ -8,14 +8,14 @@ import net.liftweb.util.SimpleInjector object Branches extends SimpleInjector { case class BranchId(value : String) - case class BranchData(branches : List[BankBranch], license : DataLicense) + case class BranchData(branches : List[Branch], license : DataLicense) trait DataLicense { def name : String def url : String } - trait BankBranch { + trait Branch { def branchId : BranchId def name : String def address : Address @@ -32,15 +32,15 @@ object Branches extends SimpleInjector { def countryCode : String } - val bankBranchesProvider = new Inject(buildOne _) {} + val branchesProvider = new Inject(buildOne _) {} - def buildOne: BankBranchesProvider = MappedBranchesProvider + def buildOne: BranchesProvider = MappedBranchesProvider } -trait BankBranchesProvider { +trait BranchesProvider { - private val logger = Logger(classOf[BankBranchesProvider]) + private val logger = Logger(classOf[BranchesProvider]) final def getBranches(bank : BankId) : Option[BranchData] = { branchDataLicense(bank) match { @@ -71,7 +71,7 @@ trait BankBranchesProvider { - protected def branchData(bank : BankId) : List[BankBranch] + protected def branchData(bank : BankId) : List[Branch] protected def branchDataLicense(bank : BankId) : Option[DataLicense] } diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index f048de413..f6f5e1210 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -1,19 +1,19 @@ package code.branches -import code.branches.Branches.{DataLicense, BranchId, Address, BankBranch} +import code.branches.Branches.{DataLicense, BranchId, Address, Branch} import code.model.BankId import code.util.DefaultStringField import net.liftweb.mapper._ -object MappedBranchesProvider extends BankBranchesProvider { - override protected def branchData(bank: BankId): List[BankBranch] = +object MappedBranchesProvider extends BranchesProvider { + override protected def branchData(bank: BankId): List[Branch] = MappedBranch.findAll(By(MappedBranch.mBankId, bank.value)) override protected def branchDataLicense(bank: BankId): Option[DataLicense] = MappedDataLicense.find(By(MappedDataLicense.mBankId, bank.value)) } -class MappedBranch extends BankBranch with LongKeyedMapper[MappedBranch] with IdPK { +class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { override def getSingleton = MappedBranch diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 12a5cdc3d..37008eb7f 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -41,21 +41,21 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls } } - protected def createSaveableBranches(data : List[SandboxBankBranchImport]) : Box[List[Saveable[BranchType]]] = { - val mappedBankBranches = data.map(bankBranch => { + protected def createSaveableBranches(data : List[SandboxBranchImport]) : Box[List[Saveable[BranchType]]] = { + val mappedBranches = data.map(branch => { MappedBranch.create - .mBranchId(bankBranch.id) - .mBankId(bankBranch.bank) - .mName(bankBranch.name) + .mBranchId(branch.id) + .mBankId(branch.bank) + .mName(branch.name) // TODO add the other fields }) - val validationErrors = mappedBankBranches.flatMap(_.validate) + val validationErrors = mappedBranches.flatMap(_.validate) if(validationErrors.nonEmpty) { Failure(s"Errors: ${validationErrors.map(_.msg)}") } else { - Full(mappedBankBranches.map(MappedSaveable(_))) + Full(mappedBranches.map(MappedSaveable(_))) } } diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index c771ee0f0..bd9075be4 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -377,7 +377,7 @@ case class SandboxBankImport( logo : String, website : String) -case class SandboxBankBranchImport( +case class SandboxBranchImport( id : String, bank: String, name : String, @@ -452,4 +452,4 @@ case class SandboxDataImport( users : List[SandboxUserImport], accounts : List[SandboxAccountImport], transactions : List[SandboxTransactionImport], - branches: List[SandboxBankBranchImport]) \ No newline at end of file + branches: List[SandboxBranchImport]) \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index da4647c01..db6f5d79c 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -2,8 +2,8 @@ package code.api.v1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchDataJson} import dispatch._ -import code.branches.Branches.{Address, BranchId, BankBranch, DataLicense} -import code.branches.{Branches, BankBranchesProvider} +import code.branches.Branches.{Address, BranchId, Branch, DataLicense} +import code.branches.{Branches, BranchesProvider} import code.model.BankId class BranchesTest extends V140ServerSetup { @@ -11,23 +11,23 @@ class BranchesTest extends V140ServerSetup { val BankWithLicense = BankId("bank-with-license") val BankWithoutLicense = BankId("bank-without-license") - case class BankBranchImpl(branchId : BranchId, name : String, address : Address) extends BankBranch + case class BranchImpl(branchId : BranchId, name : String, address : Address) extends Branch case class AddressImpl(line1 : String, line2 : String, line3 : String, line4 : String, line5 : String, postCode : String, countryCode : String) extends Address val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") val fakeAddress2 = fakeAddress1.copy(line1 = "00000") - val fakeBranch1 = BankBranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1) - val fakeBranch2 = BankBranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2) + val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1) + val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2) val fakeLicense = new DataLicense { override def name: String = "sample-license" override def url: String = "http://example.com/license" } - val mockConnector = new BankBranchesProvider { - override protected def branchData(bank: BankId): List[BankBranch] = { + val mockConnector = new BranchesProvider { + override protected def branchData(bank: BankId): List[Branch] = { bank match { // have it return branches even for the bank without a license so we can test the connector does not return them case BankWithLicense | BankWithoutLicense=> List(fakeBranch1, fakeBranch2) @@ -43,7 +43,7 @@ class BranchesTest extends V140ServerSetup { } } - def verifySameData(branch: BankBranch, branchJson : BranchJson) = { + def verifySameData(branch: Branch, branchJson : BranchJson) = { branch.name should equal (branchJson.name) branch.branchId should equal(BranchId(branchJson.id)) branch.address.line1 should equal(branchJson.address.line_1) @@ -58,13 +58,13 @@ class BranchesTest extends V140ServerSetup { override def beforeAll() { super.beforeAll() //use the mock connector - Branches.bankBranchesProvider.default.set(mockConnector) + Branches.branchesProvider.default.set(mockConnector) } override def afterAll() { super.afterAll() //reset the default connector - Branches.bankBranchesProvider.default.set(Branches.buildOne) + Branches.branchesProvider.default.set(Branches.buildOne) } feature("Getting bank branches") { diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index f121c79b0..73f061af6 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -127,7 +127,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul WORK IN PROGRESS TODO: Complete! */ - def verifyBranchCreated(branch : SandboxBankBranchImport) = { + def verifyBranchCreated(branch : SandboxBranchImport) = { val branchId = BranchId(branch.id) @@ -302,8 +302,8 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardLocation1 = SandboxLocationImport(52.556198, 13.384099) - val branch1AtBank1 = SandboxBankBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1) - val branch2AtBank1 = SandboxBankBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1) + val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1) + val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1) val standardBranches = branch1AtBank1 :: branch2AtBank1 :: Nil @@ -1503,7 +1503,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankId = BankId(bank1.id) - Branches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) + Branches.branchesProvider.vend.getBranches(bankId).size should equal(0) val branch1Json = Extraction.decompose(branch1AtBank1) From 7373dad909e0dd829fe8bec5fe5b0e57ffa3a266 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 16 Apr 2015 14:10:48 +0200 Subject: [PATCH 032/702] move ImporterAPI to new management package --- src/main/scala/bootstrap/liftweb/Boot.scala | 3 ++- src/main/scala/code/bankconnectors/Connector.scala | 2 +- .../scala/code/bankconnectors/LocalConnector.scala | 2 +- .../code/bankconnectors/LocalMappedConnector.scala | 2 +- .../code/{tesobe => management}/ImporterAPI.scala | 10 ++++------ .../{tesobe => management}/TransactionInserter.scala | 4 ++-- src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) rename src/main/scala/code/{tesobe => management}/ImporterAPI.scala (97%) rename src/main/scala/code/{tesobe => management}/TransactionInserter.scala (98%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 66f0cf2e8..3893c452f 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -32,6 +32,7 @@ Berlin 13359, Germany package bootstrap.liftweb import code.api.sandbox.SandboxApiCalls +import code.management.ImporterAPI import code.metadata.comments.MappedComment import code.metadata.counterparties.{MappedCounterpartyWhereTag, MappedCounterpartyMetadata} import code.metadata.narrative.MappedNarrative @@ -41,7 +42,7 @@ import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric import code.branches.{MappedBranch, MappedDataLicense} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} -import code.tesobe.{ImporterAPI, CashAccountAPI} +import code.tesobe.CashAccountAPI import net.liftweb._ import util._ import common._ diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 241d32e10..43a165afd 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -1,7 +1,7 @@ package code.bankconnectors +import code.management.ImporterAPI.ImporterTransaction import code.tesobe.CashTransaction -import code.tesobe.ImporterAPI.ImporterTransaction import code.util.Helper._ import com.tesobe.model.CreateBankAccount import net.liftweb.common.Box diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index c7a24d82f..6b4af220e 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -2,8 +2,8 @@ package code.bankconnectors import java.text.SimpleDateFormat import java.util.{Date, UUID, TimeZone} +import code.management.ImporterAPI.ImporterTransaction import code.tesobe.CashTransaction -import code.tesobe.ImporterAPI.ImporterTransaction import code.util.Helper import net.liftweb.common.{Failure, Box, Loggable, Full} import net.liftweb.json.Extraction diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 226c8ab0c..f77185fe5 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -6,7 +6,7 @@ import code.metadata.counterparties.Counterparties import code.model._ import code.model.dataAccess.{UpdatesRequestSender, MappedBankAccount, MappedAccountHolder, MappedBank} import code.tesobe.CashTransaction -import code.tesobe.ImporterAPI.ImporterTransaction +import code.management.ImporterAPI.ImporterTransaction import code.util.Helper import com.tesobe.model.UpdateBankAccount import net.liftweb.common.{Loggable, Full, Box} diff --git a/src/main/scala/code/tesobe/ImporterAPI.scala b/src/main/scala/code/management/ImporterAPI.scala similarity index 97% rename from src/main/scala/code/tesobe/ImporterAPI.scala rename to src/main/scala/code/management/ImporterAPI.scala index 37bfc6e5d..ad55bc58a 100644 --- a/src/main/scala/code/tesobe/ImporterAPI.scala +++ b/src/main/scala/code/management/ImporterAPI.scala @@ -1,19 +1,17 @@ -package code.tesobe +package code.management import java.util.Date import code.bankconnectors.Connector import code.model.Transaction -import code.model.dataAccess.{Account, OBPEnvelope} +import code.tesobe.ErrorMessage import net.liftweb.common.{Full, Loggable} import net.liftweb.http._ import net.liftweb.http.rest.RestHelper +import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString} import net.liftweb.json.{DefaultFormats, Extraction} -import net.liftweb.json.JsonAST.{JString, JField, JObject, JArray} -import net.liftweb.mongodb.Limit -import net.liftweb.util.Props import net.liftweb.util.Helpers._ -import net.liftweb.json.JsonDSL._ +import net.liftweb.util.Props /** * This is legacy code and does not handle edge cases very well and assumes certain things, e.g. diff --git a/src/main/scala/code/tesobe/TransactionInserter.scala b/src/main/scala/code/management/TransactionInserter.scala similarity index 98% rename from src/main/scala/code/tesobe/TransactionInserter.scala rename to src/main/scala/code/management/TransactionInserter.scala index 391200fce..62a185301 100644 --- a/src/main/scala/code/tesobe/TransactionInserter.scala +++ b/src/main/scala/code/management/TransactionInserter.scala @@ -1,8 +1,8 @@ -package code.tesobe +package code.management import code.bankconnectors.Connector import code.model.Transaction -import code.tesobe.ImporterAPI._ +import code.management.ImporterAPI._ import net.liftweb.actor.LiftActor import net.liftweb.common._ import net.liftweb.util.Helpers diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index b9947c527..7e0234d7b 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -4,8 +4,8 @@ import code.api.DefaultUsers import code.api.test.ServerSetup import code.api.util.APIUtil import code.bankconnectors.{OBPQueryParam, Connector} +import code.management.ImporterAPI.ImporterTransaction import code.tesobe.CashTransaction -import code.tesobe.ImporterAPI.ImporterTransaction import com.tesobe.model.CreateBankAccount import net.liftweb.common.{Failure, Loggable, Empty, Box} import code.model._ From 4f1b221b1afde338c2ecf5db5dcf9e716d21ed03 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 16 Apr 2015 19:02:14 +0200 Subject: [PATCH 033/702] configure connector in props file --- src/main/resources/props/sample.props.template | 6 ++++++ src/main/scala/code/bankconnectors/Connector.scala | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index f6121bd5c..9c2b364bf 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -1,6 +1,12 @@ #this is a sample props file you should edit and rename #see https://www.assembla.com/wiki/show/liftweb/Properties for all the naming options, or just use "default.props" in this same folder +#which data connector to use + +#connector=mongo +#connector=rest +connector=mapped + #you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url db.driver=org.postgresql.Driver #be sure to create your database and update the line below! diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 43a165afd..61cda0c4f 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -6,7 +6,7 @@ import code.util.Helper._ import com.tesobe.model.CreateBankAccount import net.liftweb.common.Box import code.model._ -import net.liftweb.util.SimpleInjector +import net.liftweb.util.{Props, SimpleInjector} import code.model.User import code.model.OtherBankAccount import code.model.Transaction @@ -18,7 +18,11 @@ object Connector extends SimpleInjector { val connector = new Inject(buildOne _) {} - def buildOne: Connector = LocalMappedConnector + def buildOne: Connector = + Props.get("connector").openOrThrowException("no connector set") match { + case "mapped" => LocalMappedConnector + case "mongo" => LocalConnector + } } From 27ad2630779919fc0b12b6b388053a1d082087ee Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 16 Apr 2015 19:03:14 +0200 Subject: [PATCH 034/702] started deleting accounts, test etc. moved some files into new management package --- .../resources/props/sample.props.template | 8 +++ .../scala/code/management/AccountsAPI.scala | 35 ++++++++++ src/test/scala/code/api/API121Test.scala | 3 +- .../api/LocalMappedConnectorTestSetup.scala | 7 +- .../scala/code/api/TestConnectorSetup.scala | 1 + .../code/management/AccountsAPITest.scala | 66 +++++++++++++++++++ .../{tesobe => management}/ImporterTest.scala | 6 +- .../code/sandbox/SandboxDataLoadingTest.scala | 1 + 8 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 src/main/scala/code/management/AccountsAPI.scala create mode 100644 src/test/scala/code/management/AccountsAPITest.scala rename src/test/scala/code/{tesobe => management}/ImporterTest.scala (99%) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 9c2b364bf..d699d72fe 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -63,3 +63,11 @@ allow_sandbox_data_import=true #secret key that allows access to the "data import" api. You should change this to your own secret key sandbox_data_import_secret=09ejf09jf09efje09jfe0jw23rssnkndiu733n + +#management modules + +#set this to true if you want to allow users to delete accounts +allow_account_deletion=true + +#TODO: should be used +#allow_transactions_inserting=true \ No newline at end of file diff --git a/src/main/scala/code/management/AccountsAPI.scala b/src/main/scala/code/management/AccountsAPI.scala new file mode 100644 index 000000000..3a49b9548 --- /dev/null +++ b/src/main/scala/code/management/AccountsAPI.scala @@ -0,0 +1,35 @@ +package code.management +/** + * Created by stefan on 16.04.15. + */ + +import code.api.{OBPRestHelper, APIFailure} +import code.api.util.APIUtil._ +import code.model._ +import code.model.dataAccess.Account +import code.util.Helper +import net.liftweb.common.{Box, Full, Loggable} +import net.liftweb.http._ +import net.liftweb.http.js.JE.JsRaw +import net.liftweb.http.rest.RestHelper +import net.liftweb.util.Helpers._ +import net.liftweb.util.Props + +object AccountsAPI extends OBPRestHelper with Loggable { + //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. + self: RestHelper => + + val VERSION = "v1.2.1" + + oauthServe(apiPrefix { + //deletes a bank account + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { + user => + for { + u <- user ?~ "user not found" + account <- BankAccount(bankId, accountId) + //view <- account removeView(viewId) + } yield noContentJsonResponse + } + }) +} diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index 0edc71e22..08d7713b2 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -303,6 +303,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser makeGetRequest(request) } + //get one bank account def getPrivateBankAccountDetails(bankId : String, accountId : String, viewId : String, consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { val request = v1_2Request / "banks" / bankId / "accounts" / accountId / viewId / "account" <@(consumerAndToken) makeGetRequest(request) @@ -1106,7 +1107,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser And("There are no duplicate accounts") assertNoDuplicateAccounts(publicAccountsInfo) } - scenario("we get the bank accounts the user have access to", API1_2, GetBankAccountsForAllBanks) { + scenario("we get the bank accounts the user has access to", API1_2, GetBankAccountsForAllBanks) { accountTestsSpecificDBSetup() Given("We will use an access token") When("the request is sent") diff --git a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala index c8cd52fef..2141a21af 100644 --- a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala @@ -9,8 +9,11 @@ import net.liftweb.util.Helpers._ import scala.util.Random -trait LocalMappedConnectorTestSetup extends LocalConnectorTestSetup { - +trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermissions { + //TODO: replace all these helpers with connector agnostic methods like createRandomBank + // that call Connector.createBank etc. + // (same in LocalConnectorTestSetup) + // Tests should simply use the currently selected connector override protected def createBank(id : String) : Bank = { MappedBank.create .fullBankName(randomString(5)) diff --git a/src/test/scala/code/api/TestConnectorSetup.scala b/src/test/scala/code/api/TestConnectorSetup.scala index 84b83a7e4..0392388d4 100644 --- a/src/test/scala/code/api/TestConnectorSetup.scala +++ b/src/test/scala/code/api/TestConnectorSetup.scala @@ -8,6 +8,7 @@ import net.liftweb.util.Helpers._ trait TestConnectorSetup { + //TODO: implement these right here using Connector.connector.vend and get rid of specific connector setup files protected def createBank(id : String) : Bank protected def createAccount(bankId: BankId, accountId : AccountId, currency : String) : BankAccount protected def createTransaction(account : BankAccount, startDate : Date, finishDate : Date) diff --git a/src/test/scala/code/management/AccountsAPITest.scala b/src/test/scala/code/management/AccountsAPITest.scala new file mode 100644 index 000000000..91dd21f09 --- /dev/null +++ b/src/test/scala/code/management/AccountsAPITest.scala @@ -0,0 +1,66 @@ +package code.management + +import code.api.util.APIUtil.OAuth._ +import code.api.v1_2_1.AccountsJSON +import code.api.{PrivateUser2Accounts, DefaultUsers, User1AllPrivileges} +import code.api.test.APIResponse +import code.model.BankId +import dispatch._ +import code.bankconnectors.Connector +import org.scalatest.Tag + + +/** + * Created by stefan on 16.04.15. + */ + +class AccountsAPITest extends User1AllPrivileges with DefaultUsers with PrivateUser2Accounts { + + //define Tags + object Management extends Tag("Management") + object DeleteBankAccount extends Tag("deleteBankAccount") + + //some helpers + def v1_2Request = baseRequest / "obp" / "v1.2.1" + + def deleteBankAccount(bankId : String, accountId : String, consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { + val request = v1_2Request / "banks" / bankId / "accounts" / accountId <@ (consumerAndToken) + makeDeleteRequest(request) + } + + def getBankAccountsForAllBanks(consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { + val request = v1_2Request / "accounts" <@(consumerAndToken) + makeGetRequest(request) + } + + def getPublicAccountsForAllBanks() : APIResponse= { + val request = v1_2Request / "accounts" / "public" + makeGetRequest(request) + } + + val OK: Int = 200 + val CREATED: Int = 201 + val BAD_REQUEST: Int = 400 + + //Tests start here + + feature("Delete an account resource") { + scenario("We have some accounts", Management, DeleteBankAccount) { + accountTestsSpecificDBSetup() + + //get an account + val reply = getPublicAccountsForAllBanks + reply.code should equal(OK) + + //get one of those + val account = reply.body.extract[AccountsJSON].accounts.head + + //delete it + val response = deleteBankAccount(bankId = account.bank_id, accountId = account.id, consumerAndToken = user1) + response.code should equal(OK) + + //check that it's gone + Connector.connector.vend.getBank(BankId(account.bank_id)) should equal(Nil) + } + } +} \ No newline at end of file diff --git a/src/test/scala/code/tesobe/ImporterTest.scala b/src/test/scala/code/management/ImporterTest.scala similarity index 99% rename from src/test/scala/code/tesobe/ImporterTest.scala rename to src/test/scala/code/management/ImporterTest.scala index 41760f3af..6a8cde22d 100644 --- a/src/test/scala/code/tesobe/ImporterTest.scala +++ b/src/test/scala/code/management/ImporterTest.scala @@ -1,4 +1,4 @@ -package code.tesobe +package code.management import java.text.SimpleDateFormat import java.util.TimeZone @@ -6,10 +6,10 @@ import java.util.TimeZone import code.api.DefaultConnectorTestSetup import code.api.test.{APIResponse, ServerSetup} import code.bankconnectors.Connector -import code.model.{Transaction, AccountId} +import code.model.{AccountId, Transaction} +import dispatch._ import net.liftweb.common.Loggable import net.liftweb.util.Props -import dispatch._ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSetup { diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 73f061af6..452966e30 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -61,6 +61,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul implicit val formats = Serialization.formats(NoTypeHints) + //tests running on the actual sandbox? val server = TestServer def baseRequest = host(server.host, server.port) From 7b6e7fecc8a59247dfcb54f1cfdd2c2948f85003 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 17 Apr 2015 05:59:18 +0200 Subject: [PATCH 035/702] Working on returning one branch --- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 4 +- src/main/scala/code/branches/Branches.scala | 38 +++++++++---------- .../branches/MappedBranchesProvider.scala | 6 ++- .../scala/code/api/v1_4_0/BranchesTest.scala | 2 +- .../code/sandbox/SandboxDataLoadingTest.scala | 4 +- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 5404fdcf0..3c680d086 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -3,7 +3,7 @@ package code.api.v1_4_0 import java.util.Date import code.branches.Branches -import code.branches.Branches.{Branch, DataLicense, BranchData} +import code.branches.Branches.{Branch, DataLicense, BranchesData} import code.customerinfo.{CustomerMessage, CustomerInfo} object JSONFactory1_4_0 { @@ -56,7 +56,7 @@ object JSONFactory1_4_0 { BranchJson(branch.branchId.value, branch.name, createAddressJson(branch.address)) } - def createBranchesJson(branchData : BranchData) : BranchDataJson = { + def createBranchesJson(branchData : BranchesData) : BranchDataJson = { BranchDataJson(createDataLicenseJson(branchData.license), branchData.branches.map(createBranchJson)) } diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 53136e870..a12466dc3 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -1,6 +1,6 @@ package code.branches -import code.branches.Branches.{Branch, DataLicense, BranchData} +import code.branches.Branches.{Branch, DataLicense, BranchesData, BranchData} import code.model.{BranchId, BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector @@ -8,7 +8,8 @@ import net.liftweb.util.SimpleInjector object Branches extends SimpleInjector { case class BranchId(value : String) - case class BranchData(branches : List[Branch], license : DataLicense) + case class BranchesData(branches : List[Branch], license : DataLicense) + case class BranchData(branch : Branch, license : DataLicense) trait DataLicense { def name : String @@ -42,10 +43,10 @@ trait BranchesProvider { private val logger = Logger(classOf[BranchesProvider]) - final def getBranches(bank : BankId) : Option[BranchData] = { + final def getBranches(bank : BankId) : Option[BranchesData] = { branchDataLicense(bank) match { case Some(license) => - Some(BranchData(branchData(bank), license)) + Some(BranchesData(branchesData(bank), license)) case None => { logger.info(s"No branch data license found for bank ${bank.value}") None @@ -54,24 +55,21 @@ trait BranchesProvider { } // TODO work in progress. Add singular BranchData - final def getBranch(bank : BankId, branch : BranchId) : Option[BranchData] = { - branchDataLicense(bank) match { - case Some(license) => - Some(BranchData(branchData(bank), license)) - case None => { - logger.info(s"No branch data license found for bank ${bank.value}") - None - } - } - } +// final def getBranch(bank : BankId, branch : BranchId) : Option[BranchData] = { +// // Only return the data if we have a license! +// branchDataLicense(bank) match { +// case Some(license) => +// Some(BranchData(branchData(bank, branch), license)) +// case None => { +// logger.info(s"No branch data license found for bank ${bank.value}") +// None +// } +// } +// } - - - - - - protected def branchData(bank : BankId) : List[Branch] + //protected def branchData(bank : BankId, branch : BranchId) : Branch + protected def branchesData(bank : BankId) : List[Branch] protected def branchDataLicense(bank : BankId) : Option[DataLicense] } diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index f6f5e1210..1d4b04fd9 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -6,7 +6,11 @@ import code.util.DefaultStringField import net.liftweb.mapper._ object MappedBranchesProvider extends BranchesProvider { - override protected def branchData(bank: BankId): List[Branch] = + +// override protected def branchData(branch: BranchId): Option[Branch] = +// MappedBranch.find(By(MappedBranch.mBranchId, branch.value)) + + override protected def branchesData(bank: BankId): List[Branch] = MappedBranch.findAll(By(MappedBranch.mBankId, bank.value)) override protected def branchDataLicense(bank: BankId): Option[DataLicense] = diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index db6f5d79c..1169692fe 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -27,7 +27,7 @@ class BranchesTest extends V140ServerSetup { } val mockConnector = new BranchesProvider { - override protected def branchData(bank: BankId): List[Branch] = { + override protected def branchesData(bank: BankId): List[Branch] = { bank match { // have it return branches even for the bank without a license so we can test the connector does not return them case BankWithLicense | BankWithoutLicense=> List(fakeBranch1, fakeBranch2) diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 73f061af6..5c8417146 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -130,10 +130,12 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def verifyBranchCreated(branch : SandboxBranchImport) = { val branchId = BranchId(branch.id) + //val foundBranchBox = Connector.connector.vend.getBank(branchId) + // val foundBranchBox = Branches.branchesProvider.vend.getBranches(bankId) + -// val foundBranchBox = Connector.connector.vend.getBank(branchId) // // // BankBranches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) From d03aa5dafa89baa069c67003ce710371904d8511 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 22 Apr 2015 13:00:23 +0200 Subject: [PATCH 036/702] Brances in Sandbox creation. Work in Progress --- src/main/scala/code/api/OBPRestHelper.scala | 8 +- src/main/scala/code/branches/Branches.scala | 55 +++-- .../branches/MappedBranchesProvider.scala | 13 +- src/main/scala/code/model/BankingData.scala | 10 - .../sandbox/LocalConnectorDataImport.scala | 8 + .../LocalMappedConnectorDataImport.scala | 19 ++ .../scala/code/sandbox/OBPDataImport.scala | 107 +++++++++- .../scala/code/api/v1_4_0/BranchesTest.scala | 13 ++ .../code/sandbox/SandboxDataLoadingTest.scala | 201 +++++++++--------- 9 files changed, 295 insertions(+), 139 deletions(-) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 4e5f43fe2..25cccf41f 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -85,11 +85,11 @@ trait OBPRestHelper extends RestHelper with Loggable { box match { case Full(r) => r case ParamFailure(_, _, _, apiFailure : APIFailure) => { - logger.info("API Failure: " + apiFailure.msg + " ($apiFailure.responseCode)") + logger.info("jsonResponseBoxToJsonReponse case ParamFailure says: API Failure: " + apiFailure.msg + " ($apiFailure.responseCode)") errorJsonResponse(apiFailure.msg, apiFailure.responseCode) } case Failure(msg, _, _) => { - logger.info("API Failure: " + msg) + logger.info("jsonResponseBoxToJsonReponse case Failure API Failure: " + msg) errorJsonResponse(msg) } case _ => errorJsonResponse() @@ -115,9 +115,9 @@ trait OBPRestHelper extends RestHelper with Loggable { // Check if the content-type is text/json or application/json r.json_? match { case true => - logger.info("Yes we have content-type is json") + logger.info("failIfBadJSON says: Cool, content-type is json") r.json match { - case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"Invalid JSON: $msg")) + case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"failIfBadJSON says Error: Invalid JSON: $msg")) case _ => h(r) } case false => h(r) diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index a12466dc3..7aed326ad 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -1,7 +1,9 @@ package code.branches -import code.branches.Branches.{Branch, DataLicense, BranchesData, BranchData} -import code.model.{BranchId, BankId} +// Need to import these one by one because in same package! +import code.branches.Branches.{Branch, DataLicense, BranchesData, BranchData, BranchId} + +import code.model.{BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector @@ -9,7 +11,7 @@ object Branches extends SimpleInjector { case class BranchId(value : String) case class BranchesData(branches : List[Branch], license : DataLicense) - case class BranchData(branch : Branch, license : DataLicense) + case class BranchData(branch : Option[Branch], license : DataLicense) trait DataLicense { def name : String @@ -43,34 +45,47 @@ trait BranchesProvider { private val logger = Logger(classOf[BranchesProvider]) - final def getBranches(bank : BankId) : Option[BranchesData] = { - branchDataLicense(bank) match { + + /* + Common logic for returning branches. + Implementation details in branchesData + + */ + + final def getBranches(bankId : BankId) : Option[BranchesData] = { + branchDataLicense(bankId) match { case Some(license) => - Some(BranchesData(branchesData(bank), license)) + Some(BranchesData(branchesData(bankId), license)) case None => { - logger.info(s"No branch data license found for bank ${bank.value}") + logger.warn(s"getBranches says: No branch data license found for bank ${bankId.value}") None } } } - // TODO work in progress. Add singular BranchData -// final def getBranch(bank : BankId, branch : BranchId) : Option[BranchData] = { -// // Only return the data if we have a license! -// branchDataLicense(bank) match { -// case Some(license) => -// Some(BranchData(branchData(bank, branch), license)) -// case None => { -// logger.info(s"No branch data license found for bank ${bank.value}") -// None -// } -// } -// } + /* + Return one Branch + Needs bankId so we can get the licence related to the bank (BranchId should be unique anyway) + */ + final def getBranch(bankId: BankId, branchId : BranchId) : Option[BranchData] = { + // Only return the data if we have a license! + branchDataLicense(bankId) match { + case Some(license) => + Some(BranchData(branchData(branchId), license)) + case None => { + logger.warn(s"getBranch says: No branch data license found for bank ${bankId.value}") + None + } + } + } - //protected def branchData(bank : BankId, branch : BranchId) : Branch + protected def branchData(branchId : BranchId) : Option[Branch] protected def branchesData(bank : BankId) : List[Branch] protected def branchDataLicense(bank : BankId) : Option[DataLicense] + + +// End of Trait } diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index 1d4b04fd9..958f15541 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -7,11 +7,11 @@ import net.liftweb.mapper._ object MappedBranchesProvider extends BranchesProvider { -// override protected def branchData(branch: BranchId): Option[Branch] = -// MappedBranch.find(By(MappedBranch.mBranchId, branch.value)) + override protected def branchData(branchId: BranchId): Option[Branch] = + MappedBranch.find(By(MappedBranch.mBranchId, branchId.value)) - override protected def branchesData(bank: BankId): List[Branch] = - MappedBranch.findAll(By(MappedBranch.mBankId, bank.value)) + override protected def branchesData(bankId: BankId): List[Branch] = + MappedBranch.findAll(By(MappedBranch.mBankId, bankId.value)) override protected def branchDataLicense(bank: BankId): Option[DataLicense] = MappedDataLicense.find(By(MappedDataLicense.mBankId, bank.value)) @@ -54,6 +54,11 @@ object MappedBranch extends MappedBranch with LongKeyedMetaMapper[MappedBranch] override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes } +/* +For storing the data license (conceived for open data e.g. branches) +Currently used as one license per bank for all open data? +Else could store a link to this with each open data record - or via config for each open data type + */ class MappedDataLicense extends DataLicense with LongKeyedMapper[MappedDataLicense] with IdPK { override def getSingleton = MappedDataLicense diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 1b65a4d82..8e1f01508 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -87,16 +87,6 @@ case class BankId(val value : String) { object BankId { def unapply(id : String) = Some(BankId(id)) } - -case class BranchId(val value : String) { - override def toString = value -} - -object BranchId { - def unapply(id : String) = Some(BranchId(id)) -} - - trait Bank { def bankId: BankId def shortName : String diff --git a/src/main/scala/code/sandbox/LocalConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalConnectorDataImport.scala index 798b4b704..72fdaed7d 100644 --- a/src/main/scala/code/sandbox/LocalConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalConnectorDataImport.scala @@ -18,6 +18,12 @@ case class SaveableMongoObj[T <: MongoRecord[_]](value : T) extends Saveable[T] /** * Imports data into the format used by LocalConnector (e.g. HostedBank) */ + + +/* + +Not currently using this connector so not updating it at the moment. + object LocalConnectorDataImport extends OBPDataImport with CreateViewImpls with CreateOBPUsers { type BankType = HostedBank @@ -145,3 +151,5 @@ object LocalConnectorDataImport extends OBPDataImport with CreateViewImpls with } } + +*/ diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 37008eb7f..920e7dd94 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -61,6 +61,25 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls + protected def createSaveableDataLicenses(data : List[SandboxDataLicenseImport]) : Box[List[Saveable[DataLicenseType]]] = { + val mappedDataLicenses = data.map(license => { + MappedDataLicense.create + .mBankId(license.bank) + .mName(license.name) + .mUrl(license.url) + }) + + val validationErrors = mappedDataLicenses.flatMap(_.validate) + + if(validationErrors.nonEmpty) { + Failure(s"Errors: ${validationErrors.map(_.msg)}") + } else { + Full(mappedDataLicenses.map(MappedSaveable(_))) + } + } + + + protected def createSaveableAccount(acc : SandboxAccountImport, banks : List[BankType]) : Box[Saveable[AccountType]] = { diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index bd9075be4..0b90eacc6 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -5,6 +5,7 @@ import java.util.UUID import code.bankconnectors.{OBPOffset, OBPLimit, Connector} import code.model.dataAccess.{APIUser, MappedAccountHolder, ViewImpl, OBPUser} import code.model._ +import code.branches.Branches.{Branch, DataLicense} import code.util.Helper import code.views.Views import net.liftweb.common.{Loggable, Full, Failure, Box} @@ -15,6 +16,7 @@ object OBPDataImport extends SimpleInjector { val importer = new Inject(buildOne _) {} + // TODO put this in props like main connector def buildOne : OBPDataImport = LocalMappedConnectorDataImport } @@ -42,6 +44,8 @@ trait OBPDataImport extends Loggable { type ViewType <: View type TransactionType <: TransactionUUID type AccountOwnerEmail = String + type BranchType <: Branch + type DataLicenseType <: DataLicense /** * Takes a list of boxes and returns a list of the content of the boxes if all boxes are Full, or returns @@ -63,6 +67,18 @@ trait OBPDataImport extends Loggable { */ protected def createSaveableBanks(data : List[SandboxBankImport]) : Box[List[Saveable[BankType]]] + + /** + * Create branches that can be saved. + */ + protected def createSaveableBranches(data : List[SandboxBranchImport]) : Box[List[Saveable[BranchType]]] + + + /** + * Create branches that can be saved. + */ + protected def createSaveableDataLicenses(data : List[SandboxDataLicenseImport]) : Box[List[Saveable[DataLicenseType]]] + /** * Create an owner view for account with BankId @bankId and AccountId @accountId that can be saved. */ @@ -157,6 +173,82 @@ trait OBPDataImport extends Loggable { } } + + final protected def createDataLicences(data : SandboxDataImport) = { + + + logger.info("Hello from createDataLicences") + + + + // TODO Check the data.licenses is OK before calling the following + + createSaveableDataLicenses(data.licenses) + + + /* + val existing = data.licenses.flatMap(lic => Connector.connector.vend.getDataLicense(BankId(lic.id))) + + val allIds = data.banks.map(_.id) + val emptyIds = allIds.filter(_.isEmpty) + val uniqueIds = data.banks.map(_.id).distinct + val duplicateIds = allIds diff uniqueIds + + if(!existing.isEmpty) { + val existingIds = existing.map(_.bankId.value) + Failure(s"Bank(s) with id(s) $existingIds already exist (and may have different non-id [e.g. short_name] values).") + } else if (!emptyIds.isEmpty){ + Failure(s"Bank(s) with empty ids are not allowed") + } else if(!duplicateIds.isEmpty) { + Failure(s"Banks must have unique ids. Duplicates found: $duplicateIds") + } else { + createSaveableDataLicenses(data.licenses) + } + */ + } + + + final protected def createBranches(data : SandboxDataImport) = { + + + logger.info("Hello from createBranches") + + + + // TODO Check the data.branches is OK before calling the following + + createSaveableBranches(data.branches) + + + /* + val existing = data.licenses.flatMap(lic => Connector.connector.vend.getDataLicense(BankId(lic.id))) + + val allIds = data.banks.map(_.id) + val emptyIds = allIds.filter(_.isEmpty) + val uniqueIds = data.banks.map(_.id).distinct + val duplicateIds = allIds diff uniqueIds + + if(!existing.isEmpty) { + val existingIds = existing.map(_.bankId.value) + Failure(s"Bank(s) with id(s) $existingIds already exist (and may have different non-id [e.g. short_name] values).") + } else if (!emptyIds.isEmpty){ + Failure(s"Bank(s) with empty ids are not allowed") + } else if(!duplicateIds.isEmpty) { + Failure(s"Banks must have unique ids. Duplicates found: $duplicateIds") + } else { + createSaveableDataLicenses(data.licenses) + } + */ + } + + + + + + + + + final protected def validateAccount(acc : SandboxAccountImport, data : SandboxDataImport) : Box[SandboxAccountImport] = { for { ownersNonEmpty <- Helper.booleanToBox(acc.owners.nonEmpty) ?~ @@ -339,11 +431,16 @@ trait OBPDataImport extends Loggable { users <- createUsers(data.users) accountResults <- createAccountsAndViews(data, banks.map(_.value)) transactions <- createTransactions(data, banks.map(_.value), accountResults.map(_._1.value)) + licenses <- createDataLicences(data) + branches <- createBranches(data) } yield { banks.foreach(_.save()) users.foreach(_.save()) + licenses.foreach(_.save()) + + accountResults.foreach { case (account, views, accOwnerEmails) => account.save() @@ -386,10 +483,9 @@ case class SandboxBranchImport( case class SandboxDataLicenseImport( id : String, - short_name : String, - full_name : String, - logo : String, - website : String) + bank : String, + name : String, + url : String) case class SandboxAddressImport( line_1 : String, @@ -452,4 +548,5 @@ case class SandboxDataImport( users : List[SandboxUserImport], accounts : List[SandboxAccountImport], transactions : List[SandboxTransactionImport], - branches: List[SandboxBranchImport]) \ No newline at end of file + branches: List[SandboxBranchImport], + licenses: List[SandboxDataLicenseImport]) \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index 1169692fe..879055bc5 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -35,6 +35,19 @@ class BranchesTest extends V140ServerSetup { } } + override protected def branchData(branchId: BranchId): Option[Branch] = { + branchId match { + // have it return a branch even for the bank without a license so we can test the connector does not return them + case BankWithLicense | BankWithoutLicense=> Some(fakeBranch1) + case _ => None + } + } + + + + + + override protected def branchDataLicense(bank: BankId): Option[DataLicense] = { bank match { case BankWithLicense => Some(fakeLicense) diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index a5cb7a246..45198d147 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -38,8 +38,9 @@ import code.TestServer import code.api.test.{SendServerRequests, APIResponse} import code.api.v1_2_1.APIMethods121 import code.branches.Branches +import code.branches.Branches.{BranchData, BranchId} import code.model.dataAccess._ -import code.model.{BranchId, TransactionId, AccountId, BankId} +import code.model.{TransactionId, AccountId, BankId} import code.users.Users import code.views.Views import dispatch._ @@ -84,13 +85,14 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul xs.mkString("[", ",", "]") } - def createImportJson(banks: List[JValue], users: List[JValue], accounts : List[JValue], transactions : List[JValue], branches : List[JValue]) : String = { + def createImportJson(banks: List[JValue], users: List[JValue], accounts : List[JValue], transactions : List[JValue], branches : List[JValue], licenses : List[JValue]) : String = { val json = ("banks" -> banks) ~ ("users" -> users) ~ ("accounts" -> accounts) ~ ("transactions" -> transactions) ~ - ("branches" -> branches) + ("branches" -> branches) ~ + ("licenses" -> licenses) compact(render(json)) } @@ -125,34 +127,55 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul /* - WORK IN PROGRESS TODO: Complete! + */ def verifyBranchCreated(branch : SandboxBranchImport) = { + + // Get ids from input + val bankId = BankId(branch.bank) val branchId = BranchId(branch.id) - //val foundBranchBox = Connector.connector.vend.getBank(branchId) + // Check one record found + val foundBranchCount = Branches.branchesProvider.vend.getBranch(bankId, branchId).size + foundBranchCount should equal(1) + + val foundBranchBox: Option[BranchData] = Branches.branchesProvider.vend.getBranch(bankId, branchId) + foundBranchBox.isDefined should equal(true) + + val foundBranch = foundBranchBox.get - // val foundBranchBox = Branches.branchesProvider.vend.getBranches(bankId) + //foundBranchBox.map(_.branch).map(_.name) + + foundBranch.branch.get.name should equal(branch.name) -// -// -// BankBranches.bankBranchesProvider.vend.getBranches(bankId).size should equal(0) -// -// foundBranchBox.isDefined should equal(true) -// -// val foundBranch = foundBranchBox.get + foundBranch.branch.map(_.name) should equal(branch.name) + foundBranch.branch.map(_.address) should equal(branch.address) + + // TODO check more branch fields + + + // TODO check license (need separate get function) -// -// foundBranch.bankId should equal(branchId) -// foundBranch. should equal(branch.short_name) -// foundBranch.fullName should equal(branch.full_name) -// foundBranch.logoUrl should equal(branch.logo) -// foundBranch.websiteUrl should equal(branch.website) } + +// def verifyLicenseCreated(branch : SandboxBranchImport) = { +// +// // Get ids from input +// val bankId = BankId(branch.bank) +// +// +// +// } + + + + + + def verifyUserCreated(user : SandboxUserImport) = { val foundUserBox = Users.users.vend.getUserByProviderId(defaultProvider, user.email) foundUserBox.isDefined should equal(true) @@ -304,6 +327,9 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardLocation1 = SandboxLocationImport(52.556198, 13.384099) + val license1AtBank1 = SandboxDataLicenseImport (id = "pddl", bank = bank1.id, name = "PDDL", url = "http://opendatacommons.org/licenses/pddl/") + val standardLicenses = license1AtBank1 :: Nil + val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1) val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1) @@ -410,9 +436,10 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val accounts = standardAccounts val transactions = anotherTransaction :: blankCounterpartyNameTransaction :: blankCounterpartyAccountNumberTransaction :: standardTransactions val branches = standardBranches + val licenses = standardLicenses - val importJson = SandboxDataImport(banks, users, accounts, transactions, branches) + val importJson = SandboxDataImport(banks, users, accounts, transactions, branches, licenses) val response = postImportJson(write(importJson)) response.code should equal(SUCCESS) @@ -424,7 +451,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul } it should "not allow data to be imported without a secret token" in { - val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches) + val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches, standardLicenses) val response = postImportJson(write(importJson), None) response.code should equal(403) @@ -434,7 +461,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul } it should "not allow data to be imported with an invalid secret token" in { - val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches) + val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches, standardLicenses) val badToken = "12345" badToken should not equal(theImportToken) val response = postImportJson(write(importJson), Some(badToken)) @@ -455,7 +482,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankWithoutId = removeIdField(bank1Json) def getResponse(bankJson : JValue) = { - val json = createImportJson(List(bankJson), Nil, Nil, Nil, Nil) + val json = createImportJson(List(bankJson), Nil, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -504,7 +531,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankWithSameId = addIdField(baseOtherBank, bank1.id) def getResponse(bankJsons : List[JValue]) = { - val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil) + val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -522,7 +549,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified bank already exists" in { def getResponse(bankJsons : List[JValue]) = { - val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil) + val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -543,7 +570,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "require users to have valid emails" in { def getResponse(userJson : JValue) = { - val json = createImportJson(Nil, List(userJson), Nil, Nil, Nil) + val json = createImportJson(Nil, List(userJson), Nil, Nil, Nil, Nil) postImportJson(json) } @@ -589,7 +616,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "not allow multiple users with the same email" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -635,7 +662,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified user already exists" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -655,7 +682,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a user's password is missing or empty" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -678,7 +705,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "set user passwords properly" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -698,7 +725,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) postImportJson(json) } @@ -725,7 +752,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) postImportJson(json) } @@ -754,7 +781,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) postImportJson(json) } val account1AtBank1Json = Extraction.decompose(account1AtBank1) @@ -779,7 +806,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) postImportJson(json) } @@ -796,7 +823,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) postImportJson(json) } @@ -817,7 +844,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks def getResponse(accountJsons : List[JValue]) = { - val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil) + val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil, Nil) postImportJson(json) } @@ -845,7 +872,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks def getResponse(accountJsons : List[JValue]) = { - val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil) + val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil, Nil) postImportJson(json) } @@ -874,7 +901,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -905,7 +932,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -946,7 +973,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified transaction already exists" in { def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -973,7 +1000,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1028,7 +1055,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1056,7 +1083,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1084,7 +1111,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1111,7 +1138,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1138,7 +1165,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1172,7 +1199,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1205,7 +1232,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1241,7 +1268,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1275,7 +1302,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1319,7 +1346,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1364,7 +1391,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1417,7 +1444,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1469,7 +1496,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) postImportJson(json) } @@ -1496,60 +1523,42 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul t1.otherAccount.id should equal(t2.otherAccount.id) } - - ////// - it should "require branches to have non-empty ids" in { + // Helper function expects banks and branches + def getResponse(branchJson : JValue) = { + val json = createImportJson(standardBanks.map(Extraction.decompose), Nil, Nil, Nil, List(branchJson), + standardLicenses.map(Extraction.decompose)) + postImportJson(json) + } - val (banks) = (standardBanks) - - val bankId = BankId(bank1.id) - - Branches.branchesProvider.vend.getBranches(bankId).size should equal(0) - + val bankId1 = BankId(bank1.id) val branch1Json = Extraction.decompose(branch1AtBank1) val branchWithoutId = removeIdField(branch1Json) - def getResponse(branchJson : JValue) = { - val json = createImportJson(banks.map(Extraction.decompose), Nil, Nil, Nil, List(branchJson)) - postImportJson(json) - } +// val branchesJson = Extraction.decompose(standardBranches) +// val branchesWithoutId = removeIdField(branchesJson) + // Check not exists to start with + val countBranchesBefore = Branches.branchesProvider.vend.getBranches(bankId1).size + countBranchesBefore should equal(0) + + // Check creation without Id fails getResponse(branchWithoutId).code should equal(FAILED) + // Check creation with Id succeeds getResponse(branch1Json).code should equal(SUCCESS) + // Check count after creation + val countBranchesAfter = Branches.branchesProvider.vend.getBranches(bankId1).size + countBranchesAfter should equal(1) // We expect N branches + + // TODO Check that for each license we did indeed create something good + //standardBranches.foreach(verifyLicenseCreated) + + // Check that for each branch we did indeed create something good + standardBranches.foreach(verifyBranchCreated) -// -// //no banks should have been created -// Connector.connector.vend.getBanks.size should equal(0) -// -// val bankWithEmptyId = addIdField(bankWithoutId, "") -// getResponse(bankWithEmptyId).code should equal(FAILED) -// -// //no banks should have been created -// Connector.connector.vend.getBanks.size should equal(0) -// -// //Check that the same json becomes valid when a non-empty id is added -// val validId = "foo" -// val bankWithValidId = addIdField(bankWithoutId, validId) -// val response = getResponse(bankWithValidId) -// response.code should equal(SUCCESS) -// -// //Check the bank was created -// val banks = Connector.connector.vend.getBanks -// banks.size should equal(1) -// val createdBank = banks(0) -// -// createdBank.bankId should equal(BankId(validId)) -// createdBank.shortName should equal(bank1.short_name) -// createdBank.fullName should equal(bank1.full_name) -// createdBank.logoUrl should equal(bank1.logo) -// createdBank.websiteUrl should equal(bank1.website) } - - - } From 7ce3ae7a1b1253e547d1ff55db6c1cadbc53b5f3 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 14:43:25 +0200 Subject: [PATCH 037/702] ihnore some tests if feature props are disabled --- README.md | 2 +- .../resources/props/sample.props.template | 1 + src/test/scala/code/api/API121Test.scala | 110 +++++++++--------- .../BankAccountCreationListenerTest.scala | 79 +++++++------ 4 files changed, 101 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index f62486782..9c89490b8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Our tag line is: Bank as a Platform. Transparency as an Asset. The API uses OAuth 1.0 authentication. -The project roadmap is available [here.](https://trello.com/b/O9IjhPXB/open-bank-project-api) +The project roadmap is available [here.](https://openbankproject.com/roadmap/) ## DOCUMENTATION diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index d699d72fe..969ab533a 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -14,6 +14,7 @@ db.url=jdbc:postgresql://localhost:5432/dbname?user=dbusername&password=thepassw #this is needed for oauth to work. it's important to access the api over this url, e.g. # if this is 127.0.0.1 don't use localhost to access it. +# (this needs to be an URL) hostname=http://127.0.0.1:8080 #this is only useful for running the api locally via RunWebApp diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index 08d7713b2..949218d8d 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -47,6 +47,7 @@ import APIUtil.OAuth._ import code.views.Views import net.liftweb.json.JsonAST.JString import code.api.test.APIResponse +import net.liftweb.util.Props class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser2Accounts { @@ -666,76 +667,79 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser }) } - scenario("we make a payment", Payments) { + if (Props.getBool("messageQueue.createBankAccounts") == false) { + ignore("we make a payment", Payments) {} + } else { + scenario("we make a payment", Payments) { + val testBank = createPaymentTestBank() + val bankId = testBank.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") - val testBank = createPaymentTestBank() - val bankId = testBank.bankId - val accountId1 = AccountId("__acc1") - val accountId2 = AccountId("__acc2") - createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") - createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } - def getFromAccount : BankAccount = { - BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) - } + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } - def getToAccount : BankAccount = { - BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) - } + val fromAccount = getFromAccount + val toAccount = getToAccount - val fromAccount = getFromAccount - val toAccount = getToAccount + val totalTransactionsBefore = transactionCount(fromAccount, toAccount) - val totalTransactionsBefore = transactionCount(fromAccount, toAccount) + val beforeFromBalance = fromAccount.balance + val beforeToBalance = toAccount.balance - val beforeFromBalance = fromAccount.balance - val beforeToBalance = toAccount.balance + val amt = BigDecimal("12.50") - val amt = BigDecimal("12.50") + val payJson = MakePaymentJson(toAccount.bankId.value, toAccount.accountId.value, amt.toString) - val payJson = MakePaymentJson(toAccount.bankId.value, toAccount.accountId.value, amt.toString) + val postResult = postTransaction(fromAccount.bankId.value, fromAccount.accountId.value, view, payJson, user1) - val postResult = postTransaction(fromAccount.bankId.value, fromAccount.accountId.value, view, payJson, user1) + val transId: String = (postResult.body \ "transaction_id") match { + case JString(i) => i + case _ => "" + } + transId should not equal ("") - val transId : String = (postResult.body \ "transaction_id") match { - case JString(i) => i - case _ => "" - } - transId should not equal("") - - val reply = getTransaction( + val reply = getTransaction( fromAccount.bankId.value, fromAccount.accountId.value, view, transId, user1) - Then("we should get a 200 ok code") - reply.code should equal (200) - val transJson = reply.body.extract[TransactionJSON] + Then("we should get a 200 ok code") + reply.code should equal(200) + val transJson = reply.body.extract[TransactionJSON] - val fromAccountTransAmt = transJson.details.value.amount - //the from account transaction should have a negative value - //since money left the account - And("the json we receive back should have a transaction amount equal to the amount specified to pay") - fromAccountTransAmt should equal((-amt).toString) + val fromAccountTransAmt = transJson.details.value.amount + //the from account transaction should have a negative value + //since money left the account + And("the json we receive back should have a transaction amount equal to the amount specified to pay") + fromAccountTransAmt should equal((-amt).toString) - val expectedNewFromBalance = beforeFromBalance - amt - And("the account sending the payment should have a new_balance amount equal to the previous balance minus the amount paid") - transJson.details.new_balance.amount should equal(expectedNewFromBalance.toString) - getFromAccount.balance should equal(expectedNewFromBalance) - val toAccountTransactionsReq = getTransactions(toAccount.bankId.value, toAccount.accountId.value, view, user1) - toAccountTransactionsReq.code should equal(200) - val toAccountTransactions = toAccountTransactionsReq.body.extract[TransactionsJSON] - val newestToAccountTransaction = toAccountTransactions.transactions(0) + val expectedNewFromBalance = beforeFromBalance - amt + And("the account sending the payment should have a new_balance amount equal to the previous balance minus the amount paid") + transJson.details.new_balance.amount should equal(expectedNewFromBalance.toString) + getFromAccount.balance should equal(expectedNewFromBalance) + val toAccountTransactionsReq = getTransactions(toAccount.bankId.value, toAccount.accountId.value, view, user1) + toAccountTransactionsReq.code should equal(200) + val toAccountTransactions = toAccountTransactionsReq.body.extract[TransactionsJSON] + val newestToAccountTransaction = toAccountTransactions.transactions(0) - //here amt should be positive (unlike in the transaction in the "from" account") - And("the newest transaction for the account receiving the payment should have the proper amount") - newestToAccountTransaction.details.value.amount should equal(amt.toString) + //here amt should be positive (unlike in the transaction in the "from" account") + And("the newest transaction for the account receiving the payment should have the proper amount") + newestToAccountTransaction.details.value.amount should equal(amt.toString) - And("the account receiving the payment should have the proper balance") - val expectedNewToBalance = beforeToBalance + amt - newestToAccountTransaction.details.new_balance.amount should equal(expectedNewToBalance.toString) - getToAccount.balance should equal(expectedNewToBalance) + And("the account receiving the payment should have the proper balance") + val expectedNewToBalance = beforeToBalance + amt + newestToAccountTransaction.details.new_balance.amount should equal(expectedNewToBalance.toString) + getToAccount.balance should equal(expectedNewToBalance) - And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") - transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) + And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") + transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) + } } scenario("we can't make a payment without access to the owner view", Payments) { diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala index 434c1ff76..c5aa84854 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala @@ -6,6 +6,7 @@ import code.model.{User, BankId} import code.views.Views import net.liftweb.common.Full import net.liftweb.mapper.By +import net.liftweb.util.Props import org.scalatest.Tag import com.tesobe.model.CreateBankAccount import code.model.dataAccess.{APIUser, BankAccountCreationListener} @@ -26,14 +27,14 @@ class BankAccountCreationListenerTest extends ServerSetup with DefaultConnectorT wipeTestData() } - feature("Bank account creation via AMQP messages"){ + feature("Bank account creation via AMQP messages") { val userId = "foo" val userProvider = "bar" //need to create the user for the bank accout creation process to work def getTestUser() = - APIUser.find(By(APIUser.provider_, userProvider), By(APIUser.providerId, userId)).getOrElse{ + APIUser.find(By(APIUser.provider_, userProvider), By(APIUser.providerId, userId)).getOrElse { APIUser.create. provider_(userProvider). providerId(userId). @@ -43,7 +44,7 @@ class BankAccountCreationListenerTest extends ServerSetup with DefaultConnectorT val expectedBankId = "quxbank" val accountNumber = "123456" - def thenCheckAccountCreated(user : User) = { + def thenCheckAccountCreated(user: User) = { Then("An account with the proper parameters should be created") val userAccounts = Views.views.vend.getAllAccountsUserCanSee(Full(user)) userAccounts.size should equal(1) @@ -59,56 +60,60 @@ class BankAccountCreationListenerTest extends ServerSetup with DefaultConnectorT Connector.connector.vend.getAccountHolders(BankId(expectedBankId), createdAccount.accountId) should equal(Set(user)) } + if (Props.getBool("payments_enabled", false) == false) { + ignore("a bank account is created at a bank that does not yet exist", BankAccountCreationListenerTag) {} + ignore("a bank account is created at a bank that already exists", BankAccountCreationListenerTag) {} + } else { - scenario("a bank account is created at a bank that does not yet exist", BankAccountCreationListenerTag) { - val bankIdentifier = "qux" - val user = getTestUser() + scenario("a bank account is created at a bank that does not yet exist", BankAccountCreationListenerTag) { + val bankIdentifier = "qux" + val user = getTestUser() - Given("The account doesn't already exist") - Views.views.vend.getAllAccountsUserCanSee(Full(user)).size should equal(0) + Given("The account doesn't already exist") + Views.views.vend.getAllAccountsUserCanSee(Full(user)).size should equal(0) - And("The bank in question doesn't already exist") - Connector.connector.vend.getBank(BankId(expectedBankId)).isDefined should equal(false) + And("The bank in question doesn't already exist") + Connector.connector.vend.getBank(BankId(expectedBankId)).isDefined should equal(false) - When("We create a bank account") + When("We create a bank account") - //using expectedBankId as the bank name should be okay as the behaviour should be to slugify the bank name to get the id - //what to do if this slugification results in an id collision has not been determined yet - val msgContent = CreateBankAccount(userId, userProvider, accountNumber, bankIdentifier, expectedBankId) + //using expectedBankId as the bank name should be okay as the behaviour should be to slugify the bank name to get the id + //what to do if this slugification results in an id collision has not been determined yet + val msgContent = CreateBankAccount(userId, userProvider, accountNumber, bankIdentifier, expectedBankId) - BankAccountCreationListener.createBankAccountListener ! AMQPMessage(msgContent) + BankAccountCreationListener.createBankAccountListener ! AMQPMessage(msgContent) - //sleep to give the actor time to process the message - Thread.sleep(5000) + //sleep to give the actor time to process the message + Thread.sleep(5000) - thenCheckAccountCreated(user) + thenCheckAccountCreated(user) - And("A bank should be created") - val createdBankBox = Connector.connector.vend.getBank(BankId(expectedBankId)) - createdBankBox.isDefined should equal(true) - val createdBank = createdBankBox.get - createdBank.nationalIdentifier should equal(bankIdentifier) + And("A bank should be created") + val createdBankBox = Connector.connector.vend.getBank(BankId(expectedBankId)) + createdBankBox.isDefined should equal(true) + val createdBank = createdBankBox.get + createdBank.nationalIdentifier should equal(bankIdentifier) - } + } - scenario("a bank account is created at a bank that already exists", BankAccountCreationListenerTag) { - val user = getTestUser() - Given("The account doesn't already exist") - Views.views.vend.getAllAccountsUserCanSee(Full(user)).size should equal(0) + scenario("a bank account is created at a bank that already exists", BankAccountCreationListenerTag) { + val user = getTestUser() + Given("The account doesn't already exist") + Views.views.vend.getAllAccountsUserCanSee(Full(user)).size should equal(0) - And("The bank in question already exists") - val createdBank = createBank(expectedBankId) + And("The bank in question already exists") + val createdBank = createBank(expectedBankId) - When("We create a bank account") - val msgContent = CreateBankAccount(userId, userProvider, accountNumber, createdBank.nationalIdentifier, createdBank.bankId.value) + When("We create a bank account") + val msgContent = CreateBankAccount(userId, userProvider, accountNumber, createdBank.nationalIdentifier, createdBank.bankId.value) - BankAccountCreationListener.createBankAccountListener ! AMQPMessage(msgContent) + BankAccountCreationListener.createBankAccountListener ! AMQPMessage(msgContent) - //sleep to give the actor time to process the message - Thread.sleep(5000) + //sleep to give the actor time to process the message + Thread.sleep(5000) - thenCheckAccountCreated(user) + thenCheckAccountCreated(user) + } } } - } From 2a165d79490128799219c381222c01263da4fe70 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 14:48:01 +0200 Subject: [PATCH 038/702] fix testing/parsing html5 templates --- src/test/scala/code/AppTest.scala | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/test/scala/code/AppTest.scala b/src/test/scala/code/AppTest.scala index 92842ac15..e0d85c72b 100755 --- a/src/test/scala/code/AppTest.scala +++ b/src/test/scala/code/AppTest.scala @@ -72,7 +72,11 @@ class AppTest extends TestCase("app") { file.endsWith(".xml") def handledXHtml(file: String) = - file.endsWith(".html") || file.endsWith(".htm") || file.endsWith(".xhtml") + file.endsWith(".htm") || file.endsWith(".xhtml") + + def handledHtml(file: String) = + file.endsWith(".html") + def wellFormed(file: File) { if (file.isDirectory) @@ -91,6 +95,12 @@ class AppTest extends TestCase("app") { case _ => failed = file :: failed } } + if (file.isFile && handledHtml(file.getName)) { + Html5.parse(new java.io.FileInputStream(file.getAbsolutePath)) match { + case Full(_) => // file is ok + case _ => failed = file :: failed + } + } } wellFormed(new File("src/main/webapp")) From 1a24452c835bbe5bd15a37539fef35d43ae85660 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 14:48:40 +0200 Subject: [PATCH 039/702] some small fry --- pom.xml | 1 - src/main/resources/props/sample.props.template | 3 ++- src/main/scala/code/bankconnectors/Connector.scala | 2 +- src/main/scala/code/management/TransactionInserter.scala | 7 +++---- src/main/scala/code/model/User.scala | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index f4a4cd3a5..317459567 100644 --- a/pom.xml +++ b/pom.xml @@ -224,7 +224,6 @@ -make:transitive -dependencyfile ${project.build.directory}/.scala_dependencies - -deprecation diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 969ab533a..21073a8b4 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -3,8 +3,9 @@ #which data connector to use -#connector=mongo +#connector=mongodb #connector=rest +#connector=... connector=mapped #you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 61cda0c4f..baff6e393 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -21,7 +21,7 @@ object Connector extends SimpleInjector { def buildOne: Connector = Props.get("connector").openOrThrowException("no connector set") match { case "mapped" => LocalMappedConnector - case "mongo" => LocalConnector + case "mongodb" => LocalConnector } } diff --git a/src/main/scala/code/management/TransactionInserter.scala b/src/main/scala/code/management/TransactionInserter.scala index 62a185301..273fd3d45 100644 --- a/src/main/scala/code/management/TransactionInserter.scala +++ b/src/main/scala/code/management/TransactionInserter.scala @@ -61,11 +61,10 @@ object TransactionInserter extends LiftActor with Loggable{ toMatch.obp_transaction.details.completed.`$dt`, toMatch.obp_transaction.other_account.holder) - logger.info("Insert operation id " + insertID + " # of existing matches: " + existingMatches) - + //logger.info("Insert operation id " + insertID + " # of existing matches: " + existingMatches) val numberToInsert = identicalTransactions.size - existingMatches - - logger.info("Insert operation id " + insertID + " copies being inserted: " + numberToInsert) + if(numberToInsert > 0) + logger.info("Insert operation id " + insertID + " copies being inserted: " + numberToInsert) val results = (1 to numberToInsert).map(_ => Connector.connector.vend.createImportedTransaction(toMatch)).toList diff --git a/src/main/scala/code/model/User.scala b/src/main/scala/code/model/User.scala index 7b9afee4d..8374ac5f9 100644 --- a/src/main/scala/code/model/User.scala +++ b/src/main/scala/code/model/User.scala @@ -63,7 +63,7 @@ trait User { Full() } else { - Failure("user don't have access to any view allowing to initiate transactions") + Failure("user doesn't have access to any view that allows initiating transactions") } } From 3e66a809b51cc3e1b476146cc3ebe0965a68178d Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 15:00:30 +0200 Subject: [PATCH 040/702] more conditionals (mongodb connector, cashapi test) --- src/main/scala/bootstrap/liftweb/Boot.scala | 6 +- src/test/scala/code/tesobe/CashAPITest.scala | 302 ++++++++++--------- 2 files changed, 155 insertions(+), 153 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 3893c452f..843e68e81 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -127,9 +127,9 @@ class Boot extends Loggable{ firstChoicePropsDir.flatten.toList ::: secondChoicePropsDir.flatten.toList } - - // This sets up MongoDB config - //MongoConfig.init + // This sets up MongoDB config (for the mongodb connector) + if(Props.get("connector").getOrElse("") == "mongodb") + MongoConfig.init // set up the way to connect to the relational DB we're using if (!DB.jndiJdbcConnAvailable_?) { diff --git a/src/test/scala/code/tesobe/CashAPITest.scala b/src/test/scala/code/tesobe/CashAPITest.scala index b6e3dfd74..62838849e 100644 --- a/src/test/scala/code/tesobe/CashAPITest.scala +++ b/src/test/scala/code/tesobe/CashAPITest.scala @@ -34,9 +34,6 @@ class CashAPITest extends ServerSetup with Loggable with DefaultConnectorTestSet wipeTestData() } - val CashKeyParam = "cashApplicationKey" - val validKey = Props.get(CashKeyParam).openOrThrowException("Props key CashKeyParam not found") - def fixture() = new { lazy val bank = createBank("test-bank") lazy val account = createAccount(bank.bankId, AccountId("some-account-id"), "EUR") @@ -73,159 +70,164 @@ class CashAPITest extends ServerSetup with Loggable with DefaultConnectorTestSet formats.dateFormat.format(d1) should equal(formats.dateFormat.format(d1)) } - feature("Adding cash transactions to accounts") { + val CashKeyParam = "cashApplicationKey" + val validKey = Props.get(CashKeyParam).openOr("false") //.openOrThrowException("Props key CashKeyParam not found") - scenario("Attempting to add a transaction to an existing account using an incorrect secret key") { - val f = fixture() - Given("An invalid key") - val invalidKey = validKey + "1" + if(validKey != "false") { + feature("Adding cash transactions to accounts") { - And("An account with no existing transactions") - Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).isDefined should equal(true) - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) + scenario("Attempting to add a transaction to an existing account using an incorrect secret key") { + val f = fixture() + Given("An invalid key") + val invalidKey = validKey + "1" - When("We try to add a cash transaction") - val response = addCashTransaction(f.account.uuid, f.incomingTransactionData, Some(invalidKey)) + And("An account with no existing transactions") + Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).isDefined should equal(true) + val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsBefore.size should equal(0) - Then("We should get a 401 not authorized") - response.code should equal(401) + When("We try to add a cash transaction") + val response = addCashTransaction(f.account.uuid, f.incomingTransactionData, Some(invalidKey)) + + Then("We should get a 401 not authorized") + response.code should equal(401) + + And("No transaction should be added") + val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsAfter.size should equal(0) + } + + scenario("Attempting to add a transaction to an existing account without using a secret key ") { + val f = fixture() + Given("An account with no existing transactions") + Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).isDefined should equal(true) + val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsBefore.size should equal(0) + + When("We try to add a cash transaction") + val response = addCashTransaction(f.account.uuid, f.incomingTransactionData, None) + + Then("We should get a 401 not authorized") + response.code should equal(401) + + And("No transaction should be added") + val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsAfter.size should equal(0) + } + + scenario("Attempting to add a transaction to nonexistent account using the correct secret key") { + val f = fixture() + val nonexistentAccountUUID = UUID.randomUUID().toString + + When("We try to add a cash transaction") + val response = addCashTransaction(nonexistentAccountUUID, f.incomingTransactionData, Some(validKey)) + + Then("We should get a 400") + response.code should equal(400) + } + + + scenario("Attempting to add an incoming transaction to an existing account using the correct secret key") { + val f = fixture() + Given("An account with no existing transactions") + val accountBox = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId) + accountBox.isDefined should equal(true) + + val balanceBefore = accountBox.get.balance + + val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsBefore.size should equal(0) + + When("We try to add a cash transaction") + val tData = f.incomingTransactionData + val response = addCashTransaction(f.account.uuid, tData, Some(validKey)) + + //for some reason this was originally set to 200 instead of 201, but we'll leave it that way to avoid breaking anything + Then("We should get a 200") + response.code should equal(200) + + //TODO: check response body format? + + And("The transaction should be added") + val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsAfter.size should equal(1) + + val addedTransaction = tsAfter(0) + + And("This transaction should have the appropriate values") + checkSameDate(addedTransaction.finishDate, tData.date) + addedTransaction.description should equal(Some(tData.label)) + addedTransaction.otherAccount.label should equal(tData.otherParty) + + //cash api should always set transaction type to cash + addedTransaction.transactionType should equal("cash") + + //incoming transaction should have positive value + addedTransaction.amount.toDouble should equal(tData.amount) //icky BigDecimal to double conversion here.. + + And("The account should have its balance properly updated") + val balanceAfter = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get.balance + balanceAfter.toDouble should equal(balanceBefore.toDouble + tData.amount) //icky BigDecimal to double conversion here.. + } + + scenario("Attempting to add an outgoing transaction to an existing account using the correct secret key") { + val f = fixture() + Given("An account with no existing transactions") + val accountBox = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId) + accountBox.isDefined should equal(true) + + val balanceBefore = accountBox.get.balance + + val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsBefore.size should equal(0) + + When("We try to add a cash transaction") + val tData = f.outgoingTransactionData + val response = addCashTransaction(f.account.uuid, tData, Some(validKey)) + + //for some reason this was originally set to 200 instead of 201, but we'll leave it that way to avoid breaking anything + Then("We should get a 200") + response.code should equal(200) + + //TODO: check response body format? + + And("The transaction should be added") + val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsAfter.size should equal(1) + + val addedTransaction = tsAfter(0) + + And("This transaction should have the appropriate values") + checkSameDate(addedTransaction.finishDate, tData.date) + addedTransaction.description should equal(Some(tData.label)) + addedTransaction.otherAccount.label should equal(tData.otherParty) + + //cash api should always set transaction type to cash + addedTransaction.transactionType should equal("cash") + + //outgoing transaction should have negative value + addedTransaction.amount.toDouble should equal(-1 * tData.amount) //icky BigDecimal to double conversion here.. + + And("The account should have its balance properly updated") + val balanceAfter = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get.balance + balanceAfter.toDouble should equal(balanceBefore.toDouble - tData.amount) //icky BigDecimal to double conversion here.. + } + + scenario("Sending the wrong kind of json") { + val f = fixture() + + When("We send the wrong json format") + val response = addCashTransaction(f.account.uuid, """{"foo": "bar"}""", Some(validKey)) + + Then("We should get a 400") + response.code should equal(400) + + And("No transaction should be added") + val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get + tsAfter.size should equal(0) + } - And("No transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(0) } - - scenario("Attempting to add a transaction to an existing account without using a secret key ") { - val f = fixture() - Given("An account with no existing transactions") - Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).isDefined should equal(true) - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val response = addCashTransaction(f.account.uuid, f.incomingTransactionData, None) - - Then("We should get a 401 not authorized") - response.code should equal(401) - - And("No transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(0) - } - - scenario("Attempting to add a transaction to nonexistent account using the correct secret key") { - val f = fixture() - val nonexistentAccountUUID = UUID.randomUUID().toString - - When("We try to add a cash transaction") - val response = addCashTransaction(nonexistentAccountUUID, f.incomingTransactionData, Some(validKey)) - - Then("We should get a 400") - response.code should equal(400) - } - - - scenario("Attempting to add an incoming transaction to an existing account using the correct secret key") { - val f = fixture() - Given("An account with no existing transactions") - val accountBox = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId) - accountBox.isDefined should equal(true) - - val balanceBefore = accountBox.get.balance - - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val tData = f.incomingTransactionData - val response = addCashTransaction(f.account.uuid, tData, Some(validKey)) - - //for some reason this was originally set to 200 instead of 201, but we'll leave it that way to avoid breaking anything - Then("We should get a 200") - response.code should equal(200) - - //TODO: check response body format? - - And("The transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(1) - - val addedTransaction = tsAfter(0) - - And("This transaction should have the appropriate values") - checkSameDate(addedTransaction.finishDate, tData.date) - addedTransaction.description should equal(Some(tData.label)) - addedTransaction.otherAccount.label should equal(tData.otherParty) - - //cash api should always set transaction type to cash - addedTransaction.transactionType should equal("cash") - - //incoming transaction should have positive value - addedTransaction.amount.toDouble should equal(tData.amount) //icky BigDecimal to double conversion here.. - - And("The account should have its balance properly updated") - val balanceAfter = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get.balance - balanceAfter.toDouble should equal(balanceBefore.toDouble + tData.amount) //icky BigDecimal to double conversion here.. - } - - scenario("Attempting to add an outgoing transaction to an existing account using the correct secret key") { - val f = fixture() - Given("An account with no existing transactions") - val accountBox = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId) - accountBox.isDefined should equal(true) - - val balanceBefore = accountBox.get.balance - - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val tData = f.outgoingTransactionData - val response = addCashTransaction(f.account.uuid, tData, Some(validKey)) - - //for some reason this was originally set to 200 instead of 201, but we'll leave it that way to avoid breaking anything - Then("We should get a 200") - response.code should equal(200) - - //TODO: check response body format? - - And("The transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(1) - - val addedTransaction = tsAfter(0) - - And("This transaction should have the appropriate values") - checkSameDate(addedTransaction.finishDate, tData.date) - addedTransaction.description should equal(Some(tData.label)) - addedTransaction.otherAccount.label should equal(tData.otherParty) - - //cash api should always set transaction type to cash - addedTransaction.transactionType should equal("cash") - - //outgoing transaction should have negative value - addedTransaction.amount.toDouble should equal(-1 * tData.amount) //icky BigDecimal to double conversion here.. - - And("The account should have its balance properly updated") - val balanceAfter = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get.balance - balanceAfter.toDouble should equal(balanceBefore.toDouble - tData.amount) //icky BigDecimal to double conversion here.. - } - - scenario("Sending the wrong kind of json") { - val f = fixture() - - When("We send the wrong json format") - val response = addCashTransaction(f.account.uuid, """{"foo": "bar"}""", Some(validKey)) - - Then("We should get a 400") - response.code should equal(400) - - And("No transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(0) - } - } } From e223e4483452b4a34fe5706b20d1c3a6f934a361 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 24 Apr 2015 15:19:38 +0200 Subject: [PATCH 041/702] Branch and license saving by test not passing yet --- .../scala/code/sandbox/OBPDataImport.scala | 1 + .../code/sandbox/SandboxDataLoadingTest.scala | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 0b90eacc6..009ee4ca3 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -440,6 +440,7 @@ trait OBPDataImport extends Loggable { licenses.foreach(_.save()) + branches.foreach(_.save()) accountResults.foreach { case (account, views, accOwnerEmails) => diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 45198d147..e42d9da63 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -75,8 +75,8 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul override def beforeEach() = { - //drop database tables after (before?) the tests - MongoDB.getDb(DefaultMongoIdentifier).foreach(_.dropDatabase()) + //drop database tables before + //MongoDB.getDb(DefaultMongoIdentifier).foreach(_.dropDatabase()) ToSchemify.models.foreach(_.bulkDelete_!!()) } @@ -132,17 +132,32 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul */ def verifyBranchCreated(branch : SandboxBranchImport) = { + + println("Hello from verifyBranchCreated") + + // Get ids from input val bankId = BankId(branch.bank) val branchId = BranchId(branch.id) + println(s"bankId is $bankId") + println(s"branchId is $branchId") + + // Check one record found val foundBranchCount = Branches.branchesProvider.vend.getBranch(bankId, branchId).size + + + + foundBranchCount should equal(1) val foundBranchBox: Option[BranchData] = Branches.branchesProvider.vend.getBranch(bankId, branchId) foundBranchBox.isDefined should equal(true) + + + println ("before fbb get") val foundBranch = foundBranchBox.get @@ -1560,5 +1575,6 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul // Check that for each branch we did indeed create something good standardBranches.foreach(verifyBranchCreated) + } } From 15de568b59247d1d13ba0fca80f74b6445d7f8f7 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 15:21:07 +0200 Subject: [PATCH 042/702] remove some random secret strings for more obvious fill me ins --- .../resources/props/sample.props.template | 8 +-- .../props/test.default.props.template | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/props/test.default.props.template diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 21073a8b4..87e6744f6 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -34,7 +34,7 @@ mail.smtp.port=25 #secret key that allows access to api calls to get info about oauth tokens. You should change this #to your own secret key -BankMockKey=0Msfsofo3px99v0annf09s9j032 +BankMockKey=change_me #tesobe specific settings @@ -46,10 +46,10 @@ connection.password=thepassword #secret key that allows access to the "add cash transactions" api. You should change this to your own secret key -cashApplicationKey=c0cc72b899109dh98dwh +cashApplicationKey=change_me #secret key that allows access to the "add transactions" api. You should change this to your own secret key -importer_secret=nmoiai2noth839hsioihqohihqop +importer_secret=change_me #set this to true if you want to have the api to send a message to the hbci project to refresh transactions for an account messageQueue.updateBankAccountsTransaction=false @@ -64,7 +64,7 @@ allow_sandbox_account_creation=true allow_sandbox_data_import=true #secret key that allows access to the "data import" api. You should change this to your own secret key -sandbox_data_import_secret=09ejf09jf09efje09jfe0jw23rssnkndiu733n +sandbox_data_import_secret=change_me #management modules diff --git a/src/main/resources/props/test.default.props.template b/src/main/resources/props/test.default.props.template new file mode 100644 index 000000000..53ab36768 --- /dev/null +++ b/src/main/resources/props/test.default.props.template @@ -0,0 +1,68 @@ +#this is a sample props file you should edit and rename +#see https://www.assembla.com/wiki/show/liftweb/Properties for all the naming options, or just use "default.props" in this same folder + +#which data connector to use + +#connector=mongodb +#connector=rest +connector=mapped + +#you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url +#db.driver=org.h2.Driver +#be sure to create your database and update the line below! +#db.url=jdbc:postgresql://localhost:5432/dbname?user=dbusername&password=thepassword + + +#this is needed for oauth to work. it's important to access the api over this url, e.g. +# if this is 127.0.0.1 don't use localhost to access it. +# (this needs to be an URL) +# --for tests don't set it to 127.0.0.1, for some reason +hostname=http://localhost:8016 + +#this is only useful for running the api locally via RunWebApp +#if you use it, make sure this matches your hostname port! +#if you want to change the port when running via the command line, use "mvn -Djetty.port=8089 jetty:run" instead +tests.port=8016 + +#set this to false if you don't want the api payments call to work +payments_enabled=false + +#secret key that allows access to api calls to get info about oauth tokens. You should change this +#to your own secret key +#BankMockKey=change_me + +#tesobe specific settings + +#rabbitMQ settings (used to communicate with HBCI project) +#connection.host=hostname +#connection.user=user +#connection.password=pw + +#secret key that allows access to the "add cash transactions" api. You should change this to your own secret key +#cashApplicationKey=change_me + +#secret key that allows access to the "add transactions" api. You should change this to your own secret key +importer_secret=change_me + +#set this to true if you want to have the api to send a message to the hbci project to refresh transactions for an account +messageQueue.updateBankAccountsTransaction=false + +#set this to true if you want to have the api listen for "create account" messages from the hbci project +messageQueue.createBankAccounts=false + +#set this to true if you want to allow users to create sandbox test accounts with a starting balance +allow_sandbox_account_creation=true + +#set this to true if you want to allow the "data import" api call +allow_sandbox_data_import=true + +#secret key that allows access to the "data import" api. You should change this to your own secret key +sandbox_data_import_secret=change_me + +#management modules + +#set this to true if you want to allow users to delete accounts +allow_account_deletion=true + +#TODO: should be used +#allow_transactions_inserting=true \ No newline at end of file From 38d19091822f1db8a221d1c943aea45f1181d8bb Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 19:17:09 +0200 Subject: [PATCH 043/702] fix AccountsAPI test call (give proper method for oauth signing), also actually add to dispatcher --- src/main/scala/bootstrap/liftweb/Boot.scala | 7 +++++-- src/main/scala/code/api/OBPAPI1.1.scala | 1 + src/main/scala/code/api/OBPRestHelper.scala | 4 ++-- src/main/scala/code/api/util/APIUtil.scala | 4 ++-- src/main/scala/code/management/AccountsAPI.scala | 10 ++++++---- src/test/scala/code/api/SendServerRequests.scala | 5 +++-- .../scala/code/management/AccountsAPITest.scala | 13 ++++++++----- src/test/scala/code/management/ImporterTest.scala | 2 +- 8 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 843e68e81..3bf8e9e20 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -33,6 +33,7 @@ package bootstrap.liftweb import code.api.sandbox.SandboxApiCalls import code.management.ImporterAPI +import code.management.AccountsAPI import code.metadata.comments.MappedComment import code.metadata.counterparties.{MappedCounterpartyWhereTag, MappedCounterpartyMetadata} import code.metadata.narrative.MappedNarrative @@ -182,12 +183,14 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(v1_3_0.OBPAPI1_3_0) LiftRules.statelessDispatch.append(v1_4_0.OBPAPI1_4_0) - //Tesobe specific - LiftRules.statelessDispatch.append(CashAccountAPI) + //add management apis LiftRules.statelessDispatch.append(ImporterAPI) + LiftRules.statelessDispatch.append(AccountsAPI) // add other apis + LiftRules.statelessDispatch.append(CashAccountAPI) LiftRules.statelessDispatch.append(BankMockAPI) + // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry bellow //OAuth API call LiftRules.statelessDispatch.append(OAuthHandshake) diff --git a/src/main/scala/code/api/OBPAPI1.1.scala b/src/main/scala/code/api/OBPAPI1.1.scala index 8ff166421..a89474462 100644 --- a/src/main/scala/code/api/OBPAPI1.1.scala +++ b/src/main/scala/code/api/OBPAPI1.1.scala @@ -355,6 +355,7 @@ object OBPAPI1_1 extends RestHelper with Loggable { ("views_available" -> views.map(viewToJson(_))) )) } + def bankAccountSet2JsonResponse(bankAccounts: Set[BankAccount]): LiftResponse = { val accJson = bankAccounts.map(accountToJson(_,user)) JsonResponse(("accounts" -> accJson)) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 25cccf41f..3187eaf61 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -115,9 +115,9 @@ trait OBPRestHelper extends RestHelper with Loggable { // Check if the content-type is text/json or application/json r.json_? match { case true => - logger.info("failIfBadJSON says: Cool, content-type is json") + //logger.info("failIfBadJSON says: Cool, content-type is json") r.json match { - case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"failIfBadJSON says Error: Invalid JSON: $msg")) + case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"Error: Invalid JSON: $msg")) case _ => h(r) } case false => h(r) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 54e277c93..11b7ecac5 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -94,11 +94,11 @@ object APIUtil { object OAuth { import javax.crypto -import dispatch.{Req => Request} + import dispatch.{Req => Request} import net.liftweb.util.Helpers import org.apache.http.protocol.HTTP.UTF_8 -import scala.collection.Map + import scala.collection.Map import scala.collection.immutable.{TreeMap, Map => IMap} import scala.collection.mutable.Set diff --git a/src/main/scala/code/management/AccountsAPI.scala b/src/main/scala/code/management/AccountsAPI.scala index 3a49b9548..0210d3cfd 100644 --- a/src/main/scala/code/management/AccountsAPI.scala +++ b/src/main/scala/code/management/AccountsAPI.scala @@ -1,6 +1,6 @@ package code.management /** - * Created by stefan on 16.04.15. + * Created by Stefan on 16.04.15. */ import code.api.{OBPRestHelper, APIFailure} @@ -19,17 +19,19 @@ object AccountsAPI extends OBPRestHelper with Loggable { //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. self: RestHelper => - val VERSION = "v1.2.1" + val VERSION = "management" oauthServe(apiPrefix { //deletes a bank account - case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { + case "v1.0" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { user => for { u <- user ?~ "user not found" account <- BankAccount(bankId, accountId) //view <- account removeView(viewId) - } yield noContentJsonResponse + } yield { + successJsonResponse(JsRaw("{}"), 204) + } } }) } diff --git a/src/test/scala/code/api/SendServerRequests.scala b/src/test/scala/code/api/SendServerRequests.scala index b29d49dee..be698a861 100644 --- a/src/test/scala/code/api/SendServerRequests.scala +++ b/src/test/scala/code/api/SendServerRequests.scala @@ -59,7 +59,7 @@ trait SendServerRequests { } /** - this method do a post request given a URL, a JSON and an optional Headers Map + this method do a POST request given a URL, a JSON and an optional Headers Map */ def makePostRequest(req: Req, json: String = ""): APIResponse = { req.addHeader("Content-Type", "application/json") @@ -77,7 +77,7 @@ trait SendServerRequests { } /** - * this method do a post request given a URL + * this method do a GET request given a URL */ def makeGetRequest(req: Req, params: List[(String, String)] = Nil) : APIResponse = { val jsonReq = req.GET @@ -93,6 +93,7 @@ trait SendServerRequests { * this method do a delete request given a URL */ def makeDeleteRequest(req: Req) : APIResponse = { + //Note: method will be set to late for oauth signing, so set it before using <@ val jsonReq = req.DELETE getAPIResponse(jsonReq) } diff --git a/src/test/scala/code/management/AccountsAPITest.scala b/src/test/scala/code/management/AccountsAPITest.scala index 91dd21f09..e827e3480 100644 --- a/src/test/scala/code/management/AccountsAPITest.scala +++ b/src/test/scala/code/management/AccountsAPITest.scala @@ -7,6 +7,7 @@ import code.api.test.APIResponse import code.model.BankId import dispatch._ import code.bankconnectors.Connector +import net.liftweb.common.Empty import org.scalatest.Tag @@ -22,23 +23,25 @@ class AccountsAPITest extends User1AllPrivileges with DefaultUsers with PrivateU //some helpers def v1_2Request = baseRequest / "obp" / "v1.2.1" + def managementRequest = baseRequest / "obp" / "vmanagement" / "v1.0" def deleteBankAccount(bankId : String, accountId : String, consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { - val request = v1_2Request / "banks" / bankId / "accounts" / accountId <@ (consumerAndToken) + val request = (managementRequest / "banks" / bankId / "accounts" / accountId).DELETE <@ (consumerAndToken) makeDeleteRequest(request) } def getBankAccountsForAllBanks(consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { - val request = v1_2Request / "accounts" <@(consumerAndToken) + val request = (v1_2Request / "accounts").GET <@(consumerAndToken) makeGetRequest(request) } def getPublicAccountsForAllBanks() : APIResponse= { - val request = v1_2Request / "accounts" / "public" + val request = (v1_2Request / "accounts" / "public").GET makeGetRequest(request) } val OK: Int = 200 + val OK_NO_CONTENT: Int = 204 val CREATED: Int = 201 val BAD_REQUEST: Int = 400 @@ -57,10 +60,10 @@ class AccountsAPITest extends User1AllPrivileges with DefaultUsers with PrivateU //delete it val response = deleteBankAccount(bankId = account.bank_id, accountId = account.id, consumerAndToken = user1) - response.code should equal(OK) + response.code should equal(OK_NO_CONTENT) //check that it's gone - Connector.connector.vend.getBank(BankId(account.bank_id)) should equal(Nil) + Connector.connector.vend.getBank(BankId(account.bank_id)) should equal(Empty) } } } \ No newline at end of file diff --git a/src/test/scala/code/management/ImporterTest.scala b/src/test/scala/code/management/ImporterTest.scala index 6a8cde22d..335853b7a 100644 --- a/src/test/scala/code/management/ImporterTest.scala +++ b/src/test/scala/code/management/ImporterTest.scala @@ -24,7 +24,7 @@ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSe } val secretKeyHttpParamName = "secret" - val secretKeyValue = Props.get("importer_secret").get + val secretKeyValue = Props.get("importer_secret").openOrThrowException("Prop importer_secret not specified.") val dummyKind = "Transfer" From 1985a1099cd63311203a775587b5e48b1f82a900 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 24 Apr 2015 20:34:21 +0200 Subject: [PATCH 044/702] implement removing accounts (might not be completely right) --- .../scala/code/bankconnectors/Connector.scala | 2 ++ .../code/bankconnectors/LocalConnector.scala | 17 ++++++++++++++ .../bankconnectors/LocalMappedConnector.scala | 15 ++++++++++++- .../scala/code/management/AccountsAPI.scala | 22 ++++++++++--------- src/main/scala/code/model/BankingData.scala | 12 ++++++++++ .../code/api/v1_3_0/PhysicalCardsTest.scala | 2 ++ 6 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index baff6e393..73f2edaa8 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -140,6 +140,8 @@ trait Connector { //for sandbox use -> allows us to check if we can generate a new test account with the given number def accountExists(bankId : BankId, accountNumber : String) : Boolean + //remove an account + def removeAccount(bankId: BankId, accountId: AccountId) : Boolean //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountId def getAccountByUUID(uuid : String) : Box[AccountType] diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 6b4af220e..228da1271 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -413,6 +413,23 @@ private object LocalConnector extends Connector with Loggable { } } + override def removeAccount(bankId: BankId, accountId: AccountId) : Boolean = { + import net.liftweb.mongodb.BsonDSL._ + for { + account <- Account.find((Account.bankID.name -> bankId.value) ~ (Account.accountId.value -> accountId.value)) ?~ + s"No account found with number ${accountId} at bank with id ${bankId}: could not save envelope" + } yield { + account.delete_! + } + + false +/* account + } match { + case Full(acc) => acc. + } + */ + } + //creates a bank account for an existing bank, with the appropriate values set override def createSandboxBankAccount(bankId: BankId, accountId: AccountId, accountNumber: String, currency: String, initialBalance: BigDecimal, accountHolderName: String): Box[BankAccount] = { diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index f77185fe5..acde13dea 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -12,9 +12,10 @@ import com.tesobe.model.UpdateBankAccount import net.liftweb.common.{Loggable, Full, Box} import net.liftweb.mapper._ import net.liftweb.util.Helpers._ -import net.liftweb.util.Props +import net.liftweb.util.{False, Props} import scala.concurrent.ops._ +import scala.util.Failure object LocalMappedConnector extends Connector with Loggable { @@ -229,6 +230,18 @@ object LocalMappedConnector extends Connector with Loggable { By(MappedBankAccount.accountNumber, accountNumber)) > 0 } + override def removeAccount(bankId: BankId, accountId: AccountId) : Boolean = { + val account = MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, accountId.value) + ) + + account match { + case Full(acc) => acc.delete_! + case _ => false + } + } + //creates a bank account for an existing bank, with the appropriate values set. Can fail if the bank doesn't exist override def createSandboxBankAccount(bankId: BankId, accountId: AccountId, accountNumber: String, currency: String, initialBalance: BigDecimal, accountHolderName: String): Box[BankAccount] = { diff --git a/src/main/scala/code/management/AccountsAPI.scala b/src/main/scala/code/management/AccountsAPI.scala index 0210d3cfd..9be2c7e44 100644 --- a/src/main/scala/code/management/AccountsAPI.scala +++ b/src/main/scala/code/management/AccountsAPI.scala @@ -22,16 +22,18 @@ object AccountsAPI extends OBPRestHelper with Loggable { val VERSION = "management" oauthServe(apiPrefix { - //deletes a bank account - case "v1.0" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { - user => - for { - u <- user ?~ "user not found" - account <- BankAccount(bankId, accountId) - //view <- account removeView(viewId) - } yield { + //deletes a bank account + case "v1.0" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { + user => + for { + u <- user ?~ "user not found" + account <- BankAccount(bankId, accountId) ?~ "Account not found" + } yield { + if(account.remove(u)) successJsonResponse(JsRaw("{}"), 204) - } - } + else + errorJsonResponse("{'Error': 'could not delete Account'}", 500) + } + } }) } diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 8e1f01508..afb775544 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -191,6 +191,18 @@ trait BankAccount { final def nationalIdentifier : String = Connector.connector.vend.getBank(bankId).map(_.nationalIdentifier).getOrElse("") + /* + * Delete this account (if connector allows it, e.g. local mirror of account data) + * */ + final def remove(user : User): Boolean = { + if(user.ownerAccess(this)){ + Connector.connector.vend.removeAccount(this.bankId, this.accountId) + } else { + Failure("user : " + user.emailAddress + "don't have access to owner view on account " + accountId, Empty, Empty) + false + } + } + final def owners: Set[User] = { val accountHolders = Connector.connector.vend.getAccountHolders(bankId, accountId) if(accountHolders.isEmpty) { diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index 7e0234d7b..ad8ad7d9e 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -105,6 +105,8 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { //for sandbox use -> allows us to check if we can generate a new test account with the given number override def accountExists(bankId: BankId, accountNumber: String): Boolean = ??? + override def removeAccount(bankId: BankId, accountId: AccountId) : Boolean = ??? + //creates a bank account for an existing bank, with the appropriate values set. Can fail if the bank doesn't exist override def createSandboxBankAccount(bankId: BankId, accountId: AccountId, accountNumber: String, currency: String, From 03a5f4a38d59f4c3b1ecae148e24f32e8ab650d8 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 28 Apr 2015 02:32:52 +0200 Subject: [PATCH 045/702] Different approach to license for branches license now in meta section of branch. so that branch and list of branches (or any other resource) can be treated the same. license is a meta attribute of the resource. Still WIP. --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- .../scala/code/api/v1_4_0/APIMethods140.scala | 2 +- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 25 +++-- src/main/scala/code/branches/Branches.scala | 81 ++++++++------ .../branches/MappedBranchesProvider.scala | 54 ++++++---- .../LocalMappedConnectorDataImport.scala | 36 +++---- .../scala/code/sandbox/OBPDataImport.scala | 12 +-- .../scala/code/api/v1_4_0/BranchesTest.scala | 74 ++++++------- .../branches/MappedBranchesProviderTest.scala | 13 +-- .../code/sandbox/SandboxDataLoadingTest.scala | 101 +++++------------- 10 files changed, 199 insertions(+), 203 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 843e68e81..7acb89b92 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -40,7 +40,7 @@ import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric -import code.branches.{MappedBranch, MappedDataLicense} +import code.branches.{MappedBranch} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} import code.tesobe.CashAccountAPI import net.liftweb._ @@ -338,5 +338,5 @@ object ToSchemify { MappedTransactionImage, MappedWhereTag, MappedCounterpartyMetadata, MappedCounterpartyWhereTag, MappedBank, MappedBankAccount, MappedTransaction, MappedMetric, MappedCustomerInfo, MappedCustomerMessage, - MappedBranch, MappedDataLicense) + MappedBranch) } diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 7dff0a043..0a83ecd89 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -71,7 +71,7 @@ trait APIMethods140 { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { for { - branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available", 404) + branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available. License may not be set.", 404) } yield { val json = JSONFactory1_4_0.createBranchesJson(branches) successJsonResponse(Extraction.decompose(json)) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 3c680d086..8bc17e523 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -3,7 +3,7 @@ package code.api.v1_4_0 import java.util.Date import code.branches.Branches -import code.branches.Branches.{Branch, DataLicense, BranchesData} +import code.branches.Branches.{Branch, Meta} import code.customerinfo.{CustomerMessage, CustomerInfo} object JSONFactory1_4_0 { @@ -21,9 +21,13 @@ object JSONFactory1_4_0 { case class AddCustomerMessageJson(message : String, from_department : String, from_person : String) - case class BranchDataJson(license : DataLicenseJson, branches : List[BranchJson]) + //case class BranchDataJson(license : DataLicenseJson, branches : List[BranchJson]) case class DataLicenseJson(name : String, url : String) - case class BranchJson(id : String, name : String, address : AddressJson) + + + case class BranchJson(id : String, name : String) // , address : AddressJson, meta : Meta) + case class BranchesJson (branches : List[BranchJson]) + case class AddressJson(line_1 : String, line_2 : String, line_3 : String, line_4 : String, line_5 : String, postcode_zip : String, country : String) def createCustomerInfoJson(cInfo : CustomerInfo) : CustomerInfoJson = { @@ -43,21 +47,22 @@ object JSONFactory1_4_0 { def createCustomerMessagesJson(messages : List[CustomerMessage]) : CustomerMessagesJson = { CustomerMessagesJson(messages.map(createCustomerMessageJson)) } - - def createDataLicenseJson(dataLicense : DataLicense) : DataLicenseJson = { - DataLicenseJson(dataLicense.name, dataLicense.url) - } +// +// def createDataLicenseJson(dataLicense : DataLicense) : DataLicenseJson = { +// DataLicenseJson(dataLicense.name, dataLicense.url) +// } def createAddressJson(address : Branches.Address) : AddressJson = { AddressJson(address.line1, address.line2, address.line3, address.line4, address.line5, address.postCode, address.countryCode) } def createBranchJson(branch: Branch) : BranchJson = { - BranchJson(branch.branchId.value, branch.name, createAddressJson(branch.address)) + BranchJson(branch.branchId.value, branch.name) + //, createAddressJson(branch.address), branch.meta) } - def createBranchesJson(branchData : BranchesData) : BranchDataJson = { - BranchDataJson(createDataLicenseJson(branchData.license), branchData.branches.map(createBranchJson)) + def createBranchesJson(branchesList: List[Branch]) : BranchesJson = { + BranchesJson(branchesList.map(createBranchJson)) } } diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 7aed326ad..cbbf4e4f7 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -1,27 +1,25 @@ package code.branches // Need to import these one by one because in same package! -import code.branches.Branches.{Branch, DataLicense, BranchesData, BranchData, BranchId} +import code.branches.Branches.{Branch, BranchId} import code.model.{BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector +import scala.util.Try + object Branches extends SimpleInjector { case class BranchId(value : String) - case class BranchesData(branches : List[Branch], license : DataLicense) - case class BranchData(branch : Option[Branch], license : DataLicense) - trait DataLicense { - def name : String - def url : String + trait License { + def name : String = "simon says" + def url : String = "www.google.com" } - trait Branch { - def branchId : BranchId - def name : String - def address : Address + trait Meta { + def license : License = new License {} // Note: {} used to instantiate an anonymous class of the License trait } trait Address { @@ -35,10 +33,28 @@ object Branches extends SimpleInjector { def countryCode : String } + trait Branch { + def branchId : BranchId + def name : String + //def address : Address + //def meta : Meta = new Meta {} // Note: {} used to instantiate an anonymous class of the Meta trait + } + val branchesProvider = new Inject(buildOne _) {} def buildOne: BranchesProvider = MappedBranchesProvider + + // Helper to get the count out of an option + def countOfBranches (listOpt: Option[List[Branch]]) : Int = { + val count = listOpt match { + case Some(list) => list.size + case None => 0 + } + count + } + + } trait BranchesProvider { @@ -49,18 +65,17 @@ trait BranchesProvider { /* Common logic for returning branches. Implementation details in branchesData - */ - final def getBranches(bankId : BankId) : Option[BranchesData] = { - branchDataLicense(bankId) match { - case Some(license) => - Some(BranchesData(branchesData(bankId), license)) - case None => { - logger.warn(s"getBranches says: No branch data license found for bank ${bankId.value}") - None - } - } + final def getBranches(bankId : BankId) : Option[List[Branch]] = { +// branchDataLicense(bankId) match { +// case Some(license) => + getBranchesFromProvider(bankId) +// case None => { +// logger.warn(s"getBranches says: No branch data license found for bank ${bankId.value}") +// None +// } +// } } /* @@ -68,22 +83,20 @@ trait BranchesProvider { Return one Branch Needs bankId so we can get the licence related to the bank (BranchId should be unique anyway) */ - final def getBranch(bankId: BankId, branchId : BranchId) : Option[BranchData] = { - // Only return the data if we have a license! - branchDataLicense(bankId) match { - case Some(license) => - Some(BranchData(branchData(branchId), license)) - case None => { - logger.warn(s"getBranch says: No branch data license found for bank ${bankId.value}") - None - } - } + final def getBranch(branchId : BranchId) : Option[Branch] = { +// // Only return the data if we have a license! +// branchDataLicense(bankId) match { +// case Some(license) => + getBranchFromProvider(branchId) +// case None => { +// logger.warn(s"getBranch says: No branch data license found for bank ${bankId.value}") +// None +// } + } - protected def branchData(branchId : BranchId) : Option[Branch] - protected def branchesData(bank : BankId) : List[Branch] - protected def branchDataLicense(bank : BankId) : Option[DataLicense] - + protected def getBranchFromProvider(branchId : BranchId) : Option[Branch] + protected def getBranchesFromProvider(bank : BankId) : Option[List[Branch]] // End of Trait } diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index 958f15541..708340386 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -1,26 +1,37 @@ package code.branches -import code.branches.Branches.{DataLicense, BranchId, Address, Branch} +import code.branches.Branches._ import code.model.BankId import code.util.DefaultStringField +import net.liftweb.common.Box import net.liftweb.mapper._ +import scala.util.Try + object MappedBranchesProvider extends BranchesProvider { - override protected def branchData(branchId: BranchId): Option[Branch] = + override protected def getBranchFromProvider(branchId: BranchId): Option[Branch] = MappedBranch.find(By(MappedBranch.mBranchId, branchId.value)) - override protected def branchesData(bankId: BankId): List[Branch] = - MappedBranch.findAll(By(MappedBranch.mBankId, bankId.value)) + override protected def getBranchesFromProvider(bankId: BankId): Option[List[Branch]] = { + Some(MappedBranch.findAll(By(MappedBranch.mBankId, bankId.value))) + } - override protected def branchDataLicense(bank: BankId): Option[DataLicense] = - MappedDataLicense.find(By(MappedDataLicense.mBankId, bank.value)) + + + +// override protected def branchLicense(bank: BankId): Option[License] = +// MappedDataLicense.find(By(MappedDataLicense.mBankId, bank.value)) } class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { override def getSingleton = MappedBranch + + + + object mBankId extends DefaultStringField(this) object mName extends DefaultStringField(this) @@ -35,31 +46,37 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { object mCountryCode extends MappedString(this, 2) object mPostCode extends DefaultStringField(this) + object mLicenseName extends DefaultStringField(this) + object mLicenseUrl extends DefaultStringField(this) override def branchId: BranchId = BranchId(mBranchId.get) override def name: String = mName.get - override def address: Address = new Address { - override def line1: String = mLine1.get - override def line2: String = mLine2.get - override def line3: String = mLine3.get - override def line4: String = mLine4.get - override def line5: String = mLine5.get - override def countryCode: String = mCountryCode.get - override def postCode: String = mPostCode.get - } +// override def address: Address = new Address { +// override def line1: String = mLine1.get +// override def line2: String = mLine2.get +// override def line3: String = mLine3.get +// override def line4: String = mLine4.get +// override def line5: String = mLine5.get +// override def countryCode: String = mCountryCode.get +// override def postCode: String = mPostCode.get +// } + } +// object MappedBranch extends MappedBranch with LongKeyedMetaMapper[MappedBranch] { override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes } /* -For storing the data license (conceived for open data e.g. branches) +For storing the data license(s) (conceived for open data e.g. branches) Currently used as one license per bank for all open data? Else could store a link to this with each open data record - or via config for each open data type */ -class MappedDataLicense extends DataLicense with LongKeyedMapper[MappedDataLicense] with IdPK { + + +class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdPK { override def getSingleton = MappedDataLicense object mBankId extends DefaultStringField(this) @@ -70,6 +87,7 @@ class MappedDataLicense extends DataLicense with LongKeyedMapper[MappedDataLicen override def url: String = mUrl.get } -object MappedDataLicense extends MappedDataLicense with LongKeyedMetaMapper[MappedDataLicense] { + +object MappedDataLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { override def dbIndexes = Index(mBankId) :: super.dbIndexes } \ No newline at end of file diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 920e7dd94..fc6d3f7e1 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -3,7 +3,7 @@ package code.sandbox import code.metadata.counterparties.{MappedCounterpartyMetadata} import code.model.dataAccess.{MappedBankAccount, MappedBank} import code.model.{MappedTransaction, AccountId, BankId} -import code.branches.{MappedBranch, MappedDataLicense} +import code.branches.{MappedBranch} // , MappedDataLicense import code.util.Helper.convertToSmallestCurrencyUnits import net.liftweb.common.{Full, Failure, Box} import net.liftweb.mapper.Mapper @@ -20,7 +20,7 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls type MetadataType = MappedCounterpartyMetadata type TransactionType = MappedTransaction type BranchType = MappedBranch - type DataLicenseType = MappedDataLicense + //type DataLicenseType = MappedDataLicense protected def createSaveableBanks(data : List[SandboxBankImport]) : Box[List[Saveable[BankType]]] = { val mappedBanks = data.map(bank => { @@ -61,22 +61,22 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls - protected def createSaveableDataLicenses(data : List[SandboxDataLicenseImport]) : Box[List[Saveable[DataLicenseType]]] = { - val mappedDataLicenses = data.map(license => { - MappedDataLicense.create - .mBankId(license.bank) - .mName(license.name) - .mUrl(license.url) - }) - - val validationErrors = mappedDataLicenses.flatMap(_.validate) - - if(validationErrors.nonEmpty) { - Failure(s"Errors: ${validationErrors.map(_.msg)}") - } else { - Full(mappedDataLicenses.map(MappedSaveable(_))) - } - } +// protected def createSaveableDataLicenses(data : List[SandboxDataLicenseImport]) : Box[List[Saveable[DataLicenseType]]] = { +// val mappedDataLicenses = data.map(license => { +// MappedDataLicense.create +// .mBankId(license.bank) +// .mName(license.name) +// .mUrl(license.url) +// }) +// +// val validationErrors = mappedDataLicenses.flatMap(_.validate) +// +// if(validationErrors.nonEmpty) { +// Failure(s"Errors: ${validationErrors.map(_.msg)}") +// } else { +// Full(mappedDataLicenses.map(MappedSaveable(_))) +// } +// } diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 009ee4ca3..4a352fbe6 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -5,7 +5,7 @@ import java.util.UUID import code.bankconnectors.{OBPOffset, OBPLimit, Connector} import code.model.dataAccess.{APIUser, MappedAccountHolder, ViewImpl, OBPUser} import code.model._ -import code.branches.Branches.{Branch, DataLicense} +import code.branches.Branches.{Branch} import code.util.Helper import code.views.Views import net.liftweb.common.{Loggable, Full, Failure, Box} @@ -45,7 +45,7 @@ trait OBPDataImport extends Loggable { type TransactionType <: TransactionUUID type AccountOwnerEmail = String type BranchType <: Branch - type DataLicenseType <: DataLicense + //type DataLicenseType <: DataLicense /** * Takes a list of boxes and returns a list of the content of the boxes if all boxes are Full, or returns @@ -77,7 +77,7 @@ trait OBPDataImport extends Loggable { /** * Create branches that can be saved. */ - protected def createSaveableDataLicenses(data : List[SandboxDataLicenseImport]) : Box[List[Saveable[DataLicenseType]]] + //protected def createSaveableDataLicenses(data : List[SandboxDataLicenseImport]) : Box[List[Saveable[DataLicenseType]]] /** * Create an owner view for account with BankId @bankId and AccountId @accountId that can be saved. @@ -183,7 +183,7 @@ trait OBPDataImport extends Loggable { // TODO Check the data.licenses is OK before calling the following - createSaveableDataLicenses(data.licenses) + //createSaveableDataLicenses(data.licenses) /* @@ -431,14 +431,14 @@ trait OBPDataImport extends Loggable { users <- createUsers(data.users) accountResults <- createAccountsAndViews(data, banks.map(_.value)) transactions <- createTransactions(data, banks.map(_.value), accountResults.map(_._1.value)) - licenses <- createDataLicences(data) + //licenses <- createDataLicences(data) branches <- createBranches(data) } yield { banks.foreach(_.save()) users.foreach(_.save()) - licenses.foreach(_.save()) + //licenses.foreach(_.save()) branches.foreach(_.save()) diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index 879055bc5..add7b0619 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -1,8 +1,8 @@ package code.api.v1_4_0 -import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchDataJson} +import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchesJson} import dispatch._ -import code.branches.Branches.{Address, BranchId, Branch, DataLicense} +import code.branches.Branches.{Address, BranchId, Branch, License} import code.branches.{Branches, BranchesProvider} import code.model.BankId @@ -21,21 +21,21 @@ class BranchesTest extends V140ServerSetup { val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1) val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2) - val fakeLicense = new DataLicense { + val fakeLicense = new License { override def name: String = "sample-license" override def url: String = "http://example.com/license" } val mockConnector = new BranchesProvider { - override protected def branchesData(bank: BankId): List[Branch] = { + override protected def getBranchesFromProvider(bank: BankId): Option[List[Branch]] = { bank match { // have it return branches even for the bank without a license so we can test the connector does not return them - case BankWithLicense | BankWithoutLicense=> List(fakeBranch1, fakeBranch2) - case _ => Nil + case BankWithLicense | BankWithoutLicense=> Some(List(fakeBranch1, fakeBranch2)) + case _ => None } } - override protected def branchData(branchId: BranchId): Option[Branch] = { + override protected def getBranchFromProvider(branchId: BranchId): Option[Branch] = { branchId match { // have it return a branch even for the bank without a license so we can test the connector does not return them case BankWithLicense | BankWithoutLicense=> Some(fakeBranch1) @@ -48,24 +48,24 @@ class BranchesTest extends V140ServerSetup { - override protected def branchDataLicense(bank: BankId): Option[DataLicense] = { - bank match { - case BankWithLicense => Some(fakeLicense) - case _ => None - } - } +// override protected def branchDataLicense(bank: BankId): Option[License] = { +// bank match { +// case BankWithLicense => Some(fakeLicense) +// case _ => None +// } +// } } def verifySameData(branch: Branch, branchJson : BranchJson) = { branch.name should equal (branchJson.name) branch.branchId should equal(BranchId(branchJson.id)) - branch.address.line1 should equal(branchJson.address.line_1) - branch.address.line2 should equal(branchJson.address.line_2) - branch.address.line3 should equal(branchJson.address.line_3) - branch.address.line4 should equal(branchJson.address.line_4) - branch.address.line5 should equal(branchJson.address.line_5) - branch.address.countryCode should equal(branchJson.address.country) - branch.address.postCode should equal(branchJson.address.postcode_zip) +// branch.address.line1 should equal(branchJson.address.line_1) +// branch.address.line2 should equal(branchJson.address.line_2) +// branch.address.line3 should equal(branchJson.address.line_3) +// branch.address.line4 should equal(branchJson.address.line_4) +// branch.address.line5 should equal(branchJson.address.line_5) +// branch.address.countryCode should equal(branchJson.address.country) +// branch.address.postCode should equal(branchJson.address.postcode_zip) } override def beforeAll() { @@ -101,28 +101,30 @@ class BranchesTest extends V140ServerSetup { response.code should equal(200) And("We should get the right json format") - val responseBodyOpt = response.body.extractOpt[BranchDataJson] + val responseBodyOpt = response.body.extractOpt[BranchJson] responseBodyOpt.isDefined should equal(true) val responseBody = responseBodyOpt.get And("We should get the right license") - val license = responseBody.license - license.name should equal(fakeLicense.name) - license.url should equal(fakeLicense.url) + // TODO put back in +// val license = responseBody.meta.license +// license.name should equal(fakeLicense.name) +// license.url should equal(fakeLicense.url) And("We should get the right branches") - val branches = responseBody.branches - branches.size should equal(2) - val first = branches(0) - if(first.id == fakeBranch1.branchId.value) { - verifySameData(fakeBranch1, first) - verifySameData(fakeBranch2, branches(1)) - } else if (first.id == fakeBranch2.branchId.value) { - verifySameData(fakeBranch2, first) - verifySameData(fakeBranch1, branches(1)) - } else { - fail("incorrect branches") - } + val branches = responseBody + // TODO fix this test +// branches.size should equal(2) +// val first = branches(0) +// if(first.id == fakeBranch1.branchId.value) { +// verifySameData(fakeBranch1, first) +// verifySameData(fakeBranch2, branches(1)) +// } else if (first.id == fakeBranch2.branchId.value) { +// verifySameData(fakeBranch2, first) +// verifySameData(fakeBranch1, branches(1)) +// } else { +// fail("incorrect branches") +// } } } diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index bbc9f0d71..0828b3e61 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -99,10 +99,11 @@ class MappedBranchesProviderTest extends ServerSetup { Then("We should get back the data license and the branches") branchDataOpt.isDefined should equal(true) - val branchData = branchDataOpt.get + val branches = branchDataOpt.get - branchData.license should equal(fixture.license) - branchData.branches.toSet should equal(expectedBranches) + // We no longer have one license per collection so this does not make sense + // branchData.branches. should equal(fixture.license) + branches.toSet should equal(expectedBranches) } scenario("We try to get branch data for a bank with a data license, but no branches") { @@ -122,10 +123,10 @@ class MappedBranchesProviderTest extends ServerSetup { Then("We should get back the data license, and a list branches of size 0") branchDataOpt.isDefined should equal(true) - val branchData = branchDataOpt.get + val branches = branchDataOpt.get - branchData.license should equal(license) - branchData.branches should equal(Nil) + // ditto above: branchData.license should equal(license) + branches should equal(Nil) } diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index e42d9da63..dfe9c6111 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -38,7 +38,7 @@ import code.TestServer import code.api.test.{SendServerRequests, APIResponse} import code.api.v1_2_1.APIMethods121 import code.branches.Branches -import code.branches.Branches.{BranchData, BranchId} +import code.branches.Branches.{Branch, BranchId, countOfBranches} import code.model.dataAccess._ import code.model.{TransactionId, AccountId, BankId} import code.users.Users @@ -73,7 +73,6 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val theImportToken = Props.get("sandbox_data_import_secret").openOrThrowException("sandbox_data_import_secret not set") - override def beforeEach() = { //drop database tables before //MongoDB.getDb(DefaultMongoIdentifier).foreach(_.dropDatabase()) @@ -125,17 +124,9 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul foundBank.websiteUrl should equal(bank.website) } - - /* - - - */ def verifyBranchCreated(branch : SandboxBranchImport) = { - - println("Hello from verifyBranchCreated") - // Get ids from input val bankId = BankId(branch.bank) val branchId = BranchId(branch.id) @@ -144,53 +135,21 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul println(s"branchId is $branchId") - // Check one record found - val foundBranchCount = Branches.branchesProvider.vend.getBranch(bankId, branchId).size + // check we have found a branch + val foundBranchOpt: Option[Branch] = Branches.branchesProvider.vend.getBranch(branchId) + foundBranchOpt.isDefined should equal(true) - - - foundBranchCount should equal(1) - - val foundBranchBox: Option[BranchData] = Branches.branchesProvider.vend.getBranch(bankId, branchId) - foundBranchBox.isDefined should equal(true) - - - - println ("before fbb get") - val foundBranch = foundBranchBox.get - - - //foundBranchBox.map(_.branch).map(_.name) - - foundBranch.branch.get.name should equal(branch.name) - - - foundBranch.branch.map(_.name) should equal(branch.name) - foundBranch.branch.map(_.address) should equal(branch.address) + // Check some fields + foundBranchOpt.map(_.name).get should equal(branch.name) + // foundBranch.branch.map(_.address).get should equal(branch.address) // TODO check more branch fields - - // TODO check license (need separate get function) } -// def verifyLicenseCreated(branch : SandboxBranchImport) = { -// -// // Get ids from input -// val bankId = BankId(branch.bank) -// -// -// -// } - - - - - - def verifyUserCreated(user : SandboxUserImport) = { val foundUserBox = Users.users.vend.getUserByProviderId(defaultProvider, user.email) foundUserBox.isDefined should equal(true) @@ -1538,43 +1497,41 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul t1.otherAccount.id should equal(t2.otherAccount.id) } - it should "require branches to have non-empty ids" in { + it should "create branches ok" in { // Helper function expects banks and branches - def getResponse(branchJson : JValue) = { - val json = createImportJson(standardBanks.map(Extraction.decompose), Nil, Nil, Nil, List(branchJson), - standardLicenses.map(Extraction.decompose)) - postImportJson(json) - } + def getResponse(branchList : List[JValue]) = { + val json = createImportJson(standardBanks.map(Extraction.decompose), Nil, Nil, Nil, branchList, + standardLicenses.map(Extraction.decompose)) + println(json) + postImportJson(json) + } + val bankId1 = BankId(bank1.id) - val branch1Json = Extraction.decompose(branch1AtBank1) - val branchWithoutId = removeIdField(branch1Json) + val branchesJson = standardBranches.map(Extraction.decompose) -// val branchesJson = Extraction.decompose(standardBranches) -// val branchesWithoutId = removeIdField(branchesJson) + // Check we are starting from a clean slate (no branches for this bank) + // Might be better to expect Try[List[Branch]] but then would need to modify the API stack up to the top + val existingBranches: Option[List[Branch]] = Branches.branchesProvider.vend.getBranches(bankId1) - // Check not exists to start with - val countBranchesBefore = Branches.branchesProvider.vend.getBranches(bankId1).size - countBranchesBefore should equal(0) + // We want the size of the list inside the Option + val existingBranchesCount = countOfBranches(existingBranches) + existingBranchesCount should equal (0) - // Check creation without Id fails - getResponse(branchWithoutId).code should equal(FAILED) + // Check creation succeeds + val response = getResponse(branchesJson).code + response should equal(SUCCESS) - // Check creation with Id succeeds - getResponse(branch1Json).code should equal(SUCCESS) + // Check count after creation. Again counting the items in list, not the option + val countBranchesAfter = countOfBranches(Branches.branchesProvider.vend.getBranches(bankId1)) + countBranchesAfter should equal(standardBranches.size) // We expect N branches - // Check count after creation - val countBranchesAfter = Branches.branchesProvider.vend.getBranches(bankId1).size - countBranchesAfter should equal(1) // We expect N branches - - // TODO Check that for each license we did indeed create something good + // TODO Check license situation //standardBranches.foreach(verifyLicenseCreated) // Check that for each branch we did indeed create something good standardBranches.foreach(verifyBranchCreated) - - } } From e0662a596b46c685336945f13fc0b6ee8d5d4285 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 30 Apr 2015 12:59:41 +0200 Subject: [PATCH 046/702] Progress on Branches --- src/main/scala/code/branches/Branches.scala | 8 --- src/main/scala/code/util/Helper.scala | 15 ++++++ .../scala/code/api/v1_4_0/BranchesTest.scala | 49 +++++++++++-------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index cbbf4e4f7..a2ee898fc 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -66,16 +66,8 @@ trait BranchesProvider { Common logic for returning branches. Implementation details in branchesData */ - final def getBranches(bankId : BankId) : Option[List[Branch]] = { -// branchDataLicense(bankId) match { -// case Some(license) => getBranchesFromProvider(bankId) -// case None => { -// logger.warn(s"getBranches says: No branch data license found for bank ${bankId.value}") -// None -// } -// } } /* diff --git a/src/main/scala/code/util/Helper.scala b/src/main/scala/code/util/Helper.scala index 4772625cb..733029ca0 100644 --- a/src/main/scala/code/util/Helper.scala +++ b/src/main/scala/code/util/Helper.scala @@ -4,6 +4,11 @@ import net.liftweb.common._ import net.liftweb.util.{Mailer, Props} import net.liftweb.util.Helpers._ +import net.liftweb.json.JsonAST._ +import net.liftweb.json.Extraction._ +import net.liftweb.json.Printer._ + + object Helper{ /** @@ -94,4 +99,14 @@ object Helper{ (amount * BigDecimal("10").pow(decimalPlaces)).toLong } + + + /* + Returns a pretty json representation of the input + */ + def prettyJson(input: JValue) : String = { + implicit val formats = net.liftweb.json.DefaultFormats + pretty(render(decompose(input))) + } + } \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index add7b0619..8ebde792e 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -2,24 +2,29 @@ package code.api.v1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchesJson} import dispatch._ -import code.branches.Branches.{Address, BranchId, Branch, License} +import code.branches.Branches.{BranchId, Branch, License, Address} import code.branches.{Branches, BranchesProvider} import code.model.BankId +import code.util.Helper.prettyJson class BranchesTest extends V140ServerSetup { + val BankWithLicense = BankId("bank-with-license") val BankWithoutLicense = BankId("bank-without-license") - case class BranchImpl(branchId : BranchId, name : String, address : Address) extends Branch + case class BranchImpl(branchId : BranchId, name : String) extends Branch case class AddressImpl(line1 : String, line2 : String, line3 : String, line4 : String, line5 : String, postCode : String, countryCode : String) extends Address + val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") val fakeAddress2 = fakeAddress1.copy(line1 = "00000") - val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1) - val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2) + val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1") + val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2") + + val fakeLicense = new License { override def name: String = "sample-license" @@ -100,31 +105,35 @@ class BranchesTest extends V140ServerSetup { Then("We should get a 200") response.code should equal(200) - And("We should get the right json format") - val responseBodyOpt = response.body.extractOpt[BranchJson] + And("We should get the right json format containing a list of Branches") + val wholeResponseBody = response.body + val responseBodyOpt = wholeResponseBody.extractOpt[BranchesJson] responseBodyOpt.isDefined should equal(true) + + val responseBody = responseBodyOpt.get - And("We should get the right license") + //And("We should get the right license") // TODO put back in // val license = responseBody.meta.license // license.name should equal(fakeLicense.name) // license.url should equal(fakeLicense.url) And("We should get the right branches") - val branches = responseBody - // TODO fix this test -// branches.size should equal(2) -// val first = branches(0) -// if(first.id == fakeBranch1.branchId.value) { -// verifySameData(fakeBranch1, first) -// verifySameData(fakeBranch2, branches(1)) -// } else if (first.id == fakeBranch2.branchId.value) { -// verifySameData(fakeBranch2, first) -// verifySameData(fakeBranch1, branches(1)) -// } else { -// fail("incorrect branches") -// } + val branches = responseBody.branches + + // Order of branches in the list is arbitrary + branches.size should equal(2) + val first = branches(0) + if(first.id == fakeBranch1.branchId.value) { + verifySameData(fakeBranch1, first) + verifySameData(fakeBranch2, branches(1)) + } else if (first.id == fakeBranch2.branchId.value) { + verifySameData(fakeBranch2, first) + verifySameData(fakeBranch1, branches(1)) + } else { + fail("incorrect branches") + } } } From 7e01d9bff0f0815c01f25a48569452853d9309dd Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 30 Apr 2015 14:57:31 +0200 Subject: [PATCH 047/702] fix account deletion tests --- .../scala/code/management/AccountsAPI.scala | 8 ++-- .../code/management/AccountsAPITest.scala | 47 +++++++++++-------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/main/scala/code/management/AccountsAPI.scala b/src/main/scala/code/management/AccountsAPI.scala index 9be2c7e44..62d791084 100644 --- a/src/main/scala/code/management/AccountsAPI.scala +++ b/src/main/scala/code/management/AccountsAPI.scala @@ -19,11 +19,13 @@ object AccountsAPI extends OBPRestHelper with Loggable { //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. self: RestHelper => - val VERSION = "management" + val MODULE = "internal" + val VERSION = "v1.0" + val prefix = (MODULE / VERSION ).oPrefix(_) - oauthServe(apiPrefix { + oauthServe(prefix { //deletes a bank account - case "v1.0" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonDelete json => { user => for { u <- user ?~ "user not found" diff --git a/src/test/scala/code/management/AccountsAPITest.scala b/src/test/scala/code/management/AccountsAPITest.scala index e827e3480..92eb01a5b 100644 --- a/src/test/scala/code/management/AccountsAPITest.scala +++ b/src/test/scala/code/management/AccountsAPITest.scala @@ -1,10 +1,10 @@ package code.management import code.api.util.APIUtil.OAuth._ -import code.api.v1_2_1.AccountsJSON +import code.api.v1_2_1.{AccountsJSON, API1_2_1Test} import code.api.{PrivateUser2Accounts, DefaultUsers, User1AllPrivileges} import code.api.test.APIResponse -import code.model.BankId +import code.model.{BankId, AccountId} import dispatch._ import code.bankconnectors.Connector import net.liftweb.common.Empty @@ -15,44 +15,33 @@ import org.scalatest.Tag * Created by stefan on 16.04.15. */ -class AccountsAPITest extends User1AllPrivileges with DefaultUsers with PrivateUser2Accounts { +class AccountsAPITest extends API1_2_1Test with User1AllPrivileges with DefaultUsers with PrivateUser2Accounts { //define Tags object Management extends Tag("Management") object DeleteBankAccount extends Tag("deleteBankAccount") - - //some helpers - def v1_2Request = baseRequest / "obp" / "v1.2.1" - def managementRequest = baseRequest / "obp" / "vmanagement" / "v1.0" + + def managementRequest = baseRequest / "internal" / "v1.0" def deleteBankAccount(bankId : String, accountId : String, consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { val request = (managementRequest / "banks" / bankId / "accounts" / accountId).DELETE <@ (consumerAndToken) makeDeleteRequest(request) } - def getBankAccountsForAllBanks(consumerAndToken: Option[(Consumer, Token)]) : APIResponse = { - val request = (v1_2Request / "accounts").GET <@(consumerAndToken) - makeGetRequest(request) - } - - def getPublicAccountsForAllBanks() : APIResponse= { - val request = (v1_2Request / "accounts" / "public").GET - makeGetRequest(request) - } - val OK: Int = 200 val OK_NO_CONTENT: Int = 204 val CREATED: Int = 201 val BAD_REQUEST: Int = 400 + val SERVER_ERROR: Int = 500 //Tests start here feature("Delete an account resource") { - scenario("We have some accounts", Management, DeleteBankAccount) { + scenario("User deletes one of his private accounts", Management, DeleteBankAccount) { accountTestsSpecificDBSetup() //get an account - val reply = getPublicAccountsForAllBanks + val reply = getPrivateAccountsForAllBanks(consumerAndToken = user1) reply.code should equal(OK) //get one of those @@ -63,7 +52,25 @@ class AccountsAPITest extends User1AllPrivileges with DefaultUsers with PrivateU response.code should equal(OK_NO_CONTENT) //check that it's gone - Connector.connector.vend.getBank(BankId(account.bank_id)) should equal(Empty) + Connector.connector.vend.getBankAccount(BankId(account.bank_id), AccountId(account.id)) should equal(Empty) + } + + scenario("User tries to delete a private account of another user", Management, DeleteBankAccount) { + accountTestsSpecificDBSetup() + + //get an account + val reply = getPrivateAccountsForAllBanks(consumerAndToken = user2) + reply.code should equal(OK) + + //get one of those + val account = reply.body.extract[AccountsJSON].accounts.head + + //delete it with the other user that should not have owner permissions + val response = deleteBankAccount(bankId = account.bank_id, accountId = account.id, consumerAndToken = user1) + response.code should equal(SERVER_ERROR) + + //check that it is still there + Connector.connector.vend.getBankAccount(BankId(account.bank_id), AccountId(account.id)) should not equal(Empty) } } } \ No newline at end of file From 3229e46156650a173e783cfbeb971ebc7f2f3409 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 30 Apr 2015 18:06:00 +0200 Subject: [PATCH 048/702] when deleting accounts, also delete associated transactions and metadata --- src/main/scala/code/api/oauth1.0.scala | 3 +- .../scala/code/bankconnectors/Connector.scala | 2 +- .../bankconnectors/LocalMappedConnector.scala | 85 +++++++++++++++++-- .../branches/MappedBranchesProvider.scala | 4 +- .../scala/code/management/AccountsAPI.scala | 16 ++-- .../code/management/AccountsAPITest.scala | 8 +- 6 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/main/scala/code/api/oauth1.0.scala b/src/main/scala/code/api/oauth1.0.scala index 42f86f677..39d1d3099 100644 --- a/src/main/scala/code/api/oauth1.0.scala +++ b/src/main/scala/code/api/oauth1.0.scala @@ -404,7 +404,8 @@ object OAuthHandshake extends RestHelper with Loggable { } else httpCode = 200 - logger.error("error message : " + message) + if(message.nonEmpty) + logger.error("error message : " + message) (httpCode, message, parameters) } diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 73f2edaa8..c4eac5102 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -140,7 +140,7 @@ trait Connector { //for sandbox use -> allows us to check if we can generate a new test account with the given number def accountExists(bankId : BankId, accountNumber : String) : Boolean - //remove an account + //remove an account and associated transactions def removeAccount(bankId: BankId, accountId: AccountId) : Boolean //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountId diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index acde13dea..b6d70006e 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -2,7 +2,15 @@ package code.bankconnectors import java.util.{UUID, Date} +import code.metadata.comments.MappedComment import code.metadata.counterparties.Counterparties +import code.metadata.narrative.MappedNarrative +import code.metadata.tags.MappedTag +import code.metadata.transactionimages.MappedTransactionImage +import code.metadata.wheretags.MappedWhereTag +import code.model.dataAccess.ViewImpl +import code.model.dataAccess.ViewPrivileges + import code.model._ import code.model.dataAccess.{UpdatesRequestSender, MappedBankAccount, MappedAccountHolder, MappedBank} import code.tesobe.CashTransaction @@ -230,17 +238,76 @@ object LocalMappedConnector extends Connector with Loggable { By(MappedBankAccount.accountNumber, accountNumber)) > 0 } + //remove an account and associated transactions override def removeAccount(bankId: BankId, accountId: AccountId) : Boolean = { - val account = MappedBankAccount.find( - By(MappedBankAccount.bank, bankId.value), - By(MappedBankAccount.theAccountId, accountId.value) - ) + //delete comments on transactions of this account + val commentsDeleted = MappedComment.bulkDelete_!!( + By(MappedComment.bank, bankId.value), + By(MappedComment.account, accountId.value) + ) - account match { - case Full(acc) => acc.delete_! - case _ => false - } - } + //delete narratives on transactions of this account + val narrativesDeleted = MappedNarrative.bulkDelete_!!( + By(MappedNarrative.bank, bankId.value), + By(MappedNarrative.account, accountId.value) + ) + + //delete narratives on transactions of this account + val tagsDeleted = MappedTag.bulkDelete_!!( + By(MappedTag.bank, bankId.value), + By(MappedTag.account, accountId.value) + ) + + //delete WhereTags on transactions of this account + val whereTagsDeleted = MappedWhereTag.bulkDelete_!!( + By(MappedWhereTag.bank, bankId.value), + By(MappedWhereTag.account, accountId.value) + ) + + //delete transaction images on transactions of this account + val transactionImagesDeleted = MappedTransactionImage.bulkDelete_!!( + By(MappedTransactionImage.bank, bankId.value), + By(MappedTransactionImage.account, accountId.value) + ) + + //delete transactions of account + val transactionsDeleted = MappedTransaction.bulkDelete_!!( + By(MappedTransaction.bank, bankId.value), + By(MappedTransaction.account, accountId.value) + ) + + //remove view privileges (get views first) + val views = ViewImpl.findAll( + By(ViewImpl.bankPermalink, bankId.value), + By(ViewImpl.accountPermalink, accountId.value) + ) + + //loop over them and delete + var privilegesDeleted = true + views.map (x => { + privilegesDeleted &&= ViewPrivileges.bulkDelete_!!(By(ViewPrivileges.view, x.id_)) + }) + + //delete views of account + val viewsDeleted = ViewImpl.bulkDelete_!!( + By(ViewImpl.bankPermalink, bankId.value), + By(ViewImpl.accountPermalink, accountId.value) + ) + + //delete account + val account = MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, accountId.value) + ) + + val accountDeleted = account match { + case Full(acc) => acc.delete_! + case _ => false + } + + commentsDeleted && narrativesDeleted && tagsDeleted && whereTagsDeleted && transactionImagesDeleted && + transactionsDeleted && privilegesDeleted && viewsDeleted && accountDeleted +} //creates a bank account for an existing bank, with the appropriate values set. Can fail if the bank doesn't exist override def createSandboxBankAccount(bankId: BankId, accountId: AccountId, accountNumber: String, diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index 708340386..cf613be94 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -77,7 +77,7 @@ Else could store a link to this with each open data record - or via config for e class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdPK { - override def getSingleton = MappedDataLicense + override def getSingleton = MappedLicense object mBankId extends DefaultStringField(this) object mName extends DefaultStringField(this) @@ -88,6 +88,6 @@ class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdP } -object MappedDataLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { +object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { override def dbIndexes = Index(mBankId) :: super.dbIndexes } \ No newline at end of file diff --git a/src/main/scala/code/management/AccountsAPI.scala b/src/main/scala/code/management/AccountsAPI.scala index 62d791084..f7d166caf 100644 --- a/src/main/scala/code/management/AccountsAPI.scala +++ b/src/main/scala/code/management/AccountsAPI.scala @@ -6,14 +6,9 @@ package code.management import code.api.{OBPRestHelper, APIFailure} import code.api.util.APIUtil._ import code.model._ -import code.model.dataAccess.Account -import code.util.Helper -import net.liftweb.common.{Box, Full, Loggable} -import net.liftweb.http._ +import net.liftweb.common.{Box, Full, Failure, Loggable} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.rest.RestHelper -import net.liftweb.util.Helpers._ -import net.liftweb.util.Props object AccountsAPI extends OBPRestHelper with Loggable { //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. @@ -31,10 +26,11 @@ object AccountsAPI extends OBPRestHelper with Loggable { u <- user ?~ "user not found" account <- BankAccount(bankId, accountId) ?~ "Account not found" } yield { - if(account.remove(u)) - successJsonResponse(JsRaw("{}"), 204) - else - errorJsonResponse("{'Error': 'could not delete Account'}", 500) + account.remove(u) match { + case Full(_) => successJsonResponse(JsRaw("{}"), 204) + case Failure(x, _, _) => errorJsonResponse("{'Error': '"+ x + "'}", 500) + case _ => errorJsonResponse("{'Error': 'could not delete Account'}", 500) + } } } }) diff --git a/src/test/scala/code/management/AccountsAPITest.scala b/src/test/scala/code/management/AccountsAPITest.scala index 92eb01a5b..97c3f2bb2 100644 --- a/src/test/scala/code/management/AccountsAPITest.scala +++ b/src/test/scala/code/management/AccountsAPITest.scala @@ -4,10 +4,12 @@ import code.api.util.APIUtil.OAuth._ import code.api.v1_2_1.{AccountsJSON, API1_2_1Test} import code.api.{PrivateUser2Accounts, DefaultUsers, User1AllPrivileges} import code.api.test.APIResponse +import code.model.dataAccess.ViewImpl import code.model.{BankId, AccountId} import dispatch._ import code.bankconnectors.Connector import net.liftweb.common.Empty +import net.liftweb.mapper.By import org.scalatest.Tag @@ -47,7 +49,7 @@ class AccountsAPITest extends API1_2_1Test with User1AllPrivileges with DefaultU //get one of those val account = reply.body.extract[AccountsJSON].accounts.head - //delete it + //delete the account val response = deleteBankAccount(bankId = account.bank_id, accountId = account.id, consumerAndToken = user1) response.code should equal(OK_NO_CONTENT) @@ -65,11 +67,11 @@ class AccountsAPITest extends API1_2_1Test with User1AllPrivileges with DefaultU //get one of those val account = reply.body.extract[AccountsJSON].accounts.head - //delete it with the other user that should not have owner permissions + When("Deleting the account with another user that does not have owner permissions") val response = deleteBankAccount(bankId = account.bank_id, accountId = account.id, consumerAndToken = user1) response.code should equal(SERVER_ERROR) - //check that it is still there + Then("The account should still be there") Connector.connector.vend.getBankAccount(BankId(account.bank_id), AccountId(account.id)) should not equal(Empty) } } From 1889151b036b01c52efddf89de49f82c88c4255d Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Thu, 30 Apr 2015 18:06:41 +0200 Subject: [PATCH 049/702] fix branches test to run, conditional schemify --- src/main/scala/bootstrap/liftweb/Boot.scala | 28 +++++++++---------- src/main/scala/code/model/BankingData.scala | 19 +++++++------ .../scala/code/model/dataAccess/Account.scala | 3 +- .../code/model/dataAccess/MappedBank.scala | 2 ++ .../branches/MappedBranchesProviderTest.scala | 11 ++++---- 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 6a2eec397..ead799567 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -41,7 +41,7 @@ import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric -import code.branches.{MappedBranch} +import code.branches.{MappedLicense, MappedBranch} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} import code.tesobe.CashAccountAPI import net.liftweb._ @@ -128,23 +128,19 @@ class Boot extends Loggable{ firstChoicePropsDir.flatten.toList ::: secondChoicePropsDir.flatten.toList } - // This sets up MongoDB config (for the mongodb connector) - if(Props.get("connector").getOrElse("") == "mongodb") - MongoConfig.init - - // set up the way to connect to the relational DB we're using + // set up the way to connect to the relational DB we're using (ok if other connector than relational) if (!DB.jndiJdbcConnAvailable_?) { val driver = Props.mode match { - case Props.RunModes.Production | Props.RunModes.Staging | Props.RunModes.Development => Props.get("db.driver") openOr "org.h2.Driver" + case Props.RunModes.Production | Props.RunModes.Staging | Props.RunModes.Development => Props.get("db.driver") openOr "org.h2.Driver" case _ => "org.h2.Driver" } val vendor = Props.mode match { case Props.RunModes.Production | Props.RunModes.Staging | Props.RunModes.Development => new StandardDBVendor(driver, - Props.get("db.url") openOr "jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE", - Props.get("db.user"), Props.get("db.password")) + Props.get("db.url") openOr "jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE", + Props.get("db.user"), Props.get("db.password")) case _ => new StandardDBVendor( driver, @@ -158,6 +154,14 @@ class Boot extends Loggable{ DB.defineConnectionManager(net.liftweb.util.DefaultConnectionIdentifier, vendor) } + // ensure our relational database's tables are created/fit the schema + if(Props.get("connector").getOrElse("") == "mapped") + schemifyAll() + + // This sets up MongoDB config (for the mongodb connector) + if(Props.get("connector").getOrElse("") == "mongodb") + MongoConfig.init + val runningMode = Props.mode match { case Props.RunModes.Production => "Production mode" case Props.RunModes.Staging => "Staging mode" @@ -168,10 +172,6 @@ class Boot extends Loggable{ logger.info("running mode: " + runningMode) - - // ensure our relational database's tables are created/fit the schema - schemifyAll() - // where to search snippet LiftRules.addToPackages("code") @@ -341,5 +341,5 @@ object ToSchemify { MappedTransactionImage, MappedWhereTag, MappedCounterpartyMetadata, MappedCounterpartyWhereTag, MappedBank, MappedBankAccount, MappedTransaction, MappedMetric, MappedCustomerInfo, MappedCustomerMessage, - MappedBranch) + MappedBranch, MappedLicense) } diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index afb775544..7c0b52dfa 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -87,6 +87,7 @@ case class BankId(val value : String) { object BankId { def unapply(id : String) = Some(BankId(id)) } + trait Bank { def bankId: BankId def shortName : String @@ -94,7 +95,10 @@ trait Bank { def logoUrl : String def websiteUrl : String - //it's not entirely clear what this is/represents + //SWIFT BIC banking code (globally unique) + def swiftBic: String + + //it's not entirely clear what this is/represents (BLZ in Germany?) def nationalIdentifier : String def accounts(user : Box[User]) : List[BankAccount] = { @@ -170,16 +174,16 @@ trait BankAccount { @deprecated def uuid : String - def accountId : AccountId + def accountId : AccountId //an identifier that hides the actual account number (obp identifier) def accountType : String def balance : BigDecimal def currency : String def name : String def label : String - def swift_bic : Option[String] + def swift_bic : Option[String] //TODO: deduplication, bank field should not be in account fields def iban : Option[String] - def number : String - def bankId : BankId + def number : String //the actual number as given by the bank + def bankId : BankId //bank identifier, usually short name of @deprecated("Get the account holder(s) via owners") def accountHolder : String @@ -194,12 +198,11 @@ trait BankAccount { /* * Delete this account (if connector allows it, e.g. local mirror of account data) * */ - final def remove(user : User): Boolean = { + final def remove(user : User): Box[Boolean] = { if(user.ownerAccess(this)){ - Connector.connector.vend.removeAccount(this.bankId, this.accountId) + Full(Connector.connector.vend.removeAccount(this.bankId, this.accountId)) } else { Failure("user : " + user.emailAddress + "don't have access to owner view on account " + accountId, Empty, Empty) - false } } diff --git a/src/main/scala/code/model/dataAccess/Account.scala b/src/main/scala/code/model/dataAccess/Account.scala index 17ce4a5a7..62ceee5c7 100644 --- a/src/main/scala/code/model/dataAccess/Account.scala +++ b/src/main/scala/code/model/dataAccess/Account.scala @@ -193,7 +193,7 @@ class HostedBank extends Bank with MongoRecord[HostedBank] with ObjectIdPk[Hoste object website extends StringField(this, 255) object email extends StringField(this, 255) object permalink extends StringField(this, 255) - object SWIFT_BIC extends StringField(this, 255) + object swiftBIC extends StringField(this, 255) object national_identifier extends StringField(this, 255) def getAccount(bankAccountId: AccountId) : Box[Account] = { @@ -208,6 +208,7 @@ class HostedBank extends Bank with MongoRecord[HostedBank] with ObjectIdPk[Hoste override def fullName: String = name.get override def logoUrl: String = logoURL.get override def websiteUrl: String = website.get + override def swiftBic: String = swiftBIC.get override def nationalIdentifier: String = national_identifier.get } diff --git a/src/main/scala/code/model/dataAccess/MappedBank.scala b/src/main/scala/code/model/dataAccess/MappedBank.scala index 0674d39e2..980806cb3 100644 --- a/src/main/scala/code/model/dataAccess/MappedBank.scala +++ b/src/main/scala/code/model/dataAccess/MappedBank.scala @@ -11,6 +11,7 @@ class MappedBank extends Bank with LongKeyedMapper[MappedBank] with IdPK with Cr object shortBankName extends MappedString(this, 100) object logoURL extends MappedString(this, 255) object websiteURL extends MappedString(this, 255) + object swiftBIC extends MappedString(this, 255) object national_identifier extends MappedString(this, 255) override def bankId: BankId = BankId(permalink.get) @@ -18,6 +19,7 @@ class MappedBank extends Bank with LongKeyedMapper[MappedBank] with IdPK with Cr override def shortName: String = shortBankName.get override def logoUrl: String = logoURL.get override def websiteUrl: String = websiteURL.get + override def swiftBic: String = swiftBIC.get override def nationalIdentifier: String = national_identifier.get } diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index 0828b3e61..b2e147c04 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -8,7 +8,7 @@ class MappedBranchesProviderTest extends ServerSetup { private def delete(): Unit = { MappedBranch.bulkDelete_!!() - MappedDataLicense.bulkDelete_!!() + MappedLicense.bulkDelete_!!() } override def beforeAll() = { @@ -26,7 +26,7 @@ class MappedBranchesProviderTest extends ServerSetup { val bankIdWithLicenseAndData = "some-bank" val bankIdWithNoLicense = "unlicensed-bank" - val license = MappedDataLicense.create + val license = MappedLicense.create .mBankId(bankIdWithLicenseAndData) .mName("some-license") .mUrl("http://www.example.com/license").saveMe() @@ -74,7 +74,7 @@ class MappedBranchesProviderTest extends ServerSetup { val fixture = defaultSetup() Given("The bank in question has no data license") - MappedDataLicense.count(By(MappedDataLicense.mBankId, fixture.bankIdWithNoLicense)) should equal(0) + MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithNoLicense)) should equal(0) And("The bank in question has branches") MappedBranch.find(By(MappedBranch.mBankId, fixture.bankIdWithNoLicense)).isDefined should equal(true) @@ -84,14 +84,13 @@ class MappedBranchesProviderTest extends ServerSetup { Then("We should get an empty option") branchData should equal(None) - } scenario("We try to get branch data for a bank which does have a data license set") { val fixture = defaultSetup() val expectedBranches = Set(fixture.branch1, fixture.branch2) Given("We have a data license and branches for a bank") - MappedDataLicense.count(By(MappedDataLicense.mBankId, fixture.bankIdWithLicenseAndData)) should equal(1) + MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithLicenseAndData)) should equal(1) MappedBranch.findAll(By(MappedBranch.mBankId, fixture.bankIdWithLicenseAndData)).toSet should equal(expectedBranches) When("We try to get the branch data for that bank") @@ -111,7 +110,7 @@ class MappedBranchesProviderTest extends ServerSetup { Given("We have a data license for a bank, but no branches") val bankWithNoBranches = "bank-with-no-branches" - val license = MappedDataLicense.create + val license = MappedLicense.create .mBankId(bankWithNoBranches) .mName("some-license") .mUrl("http://www.example.com/license").saveMe() From 1fd3c3017a7cd21a3e0dc07f6dd66662a09de4e3 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 1 May 2015 15:59:07 +0200 Subject: [PATCH 050/702] Storing license info with each Branch, adding address. --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 7 +- src/main/scala/code/branches/Branches.scala | 11 +- .../branches/MappedBranchesProvider.scala | 68 ++++++----- .../LocalMappedConnectorDataImport.scala | 12 +- .../scala/code/sandbox/OBPDataImport.scala | 26 ++-- .../scala/code/api/v1_4_0/BranchesTest.scala | 35 +++--- .../branches/MappedBranchesProviderTest.scala | 36 +++--- .../code/sandbox/SandboxDataLoadingTest.scala | 111 +++++++++--------- 9 files changed, 175 insertions(+), 135 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index ead799567..d27a489f1 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -41,7 +41,7 @@ import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric -import code.branches.{MappedLicense, MappedBranch} +import code.branches.{MappedBranch} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} import code.tesobe.CashAccountAPI import net.liftweb._ @@ -341,5 +341,5 @@ object ToSchemify { MappedTransactionImage, MappedWhereTag, MappedCounterpartyMetadata, MappedCounterpartyWhereTag, MappedBank, MappedBankAccount, MappedTransaction, MappedMetric, MappedCustomerInfo, MappedCustomerMessage, - MappedBranch, MappedLicense) + MappedBranch) } diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 8bc17e523..2a2f54650 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -25,10 +25,10 @@ object JSONFactory1_4_0 { case class DataLicenseJson(name : String, url : String) - case class BranchJson(id : String, name : String) // , address : AddressJson, meta : Meta) + case class BranchJson(id : String, name : String, address : AddressJson, meta : Meta) case class BranchesJson (branches : List[BranchJson]) - case class AddressJson(line_1 : String, line_2 : String, line_3 : String, line_4 : String, line_5 : String, postcode_zip : String, country : String) + case class AddressJson(line_1 : String, line_2 : String, line_3 : String, line_4 : String, line_5 : String, postcode : String, country : String) def createCustomerInfoJson(cInfo : CustomerInfo) : CustomerInfoJson = { @@ -57,8 +57,7 @@ object JSONFactory1_4_0 { } def createBranchJson(branch: Branch) : BranchJson = { - BranchJson(branch.branchId.value, branch.name) - //, createAddressJson(branch.address), branch.meta) + BranchJson(branch.branchId.value, branch.name, createAddressJson(branch.address), branch.meta) } def createBranchesJson(branchesList: List[Branch]) : BranchesJson = { diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index a2ee898fc..5b8ff3739 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -14,14 +14,15 @@ object Branches extends SimpleInjector { case class BranchId(value : String) trait License { - def name : String = "simon says" - def url : String = "www.google.com" + def name : String + def url : String } trait Meta { - def license : License = new License {} // Note: {} used to instantiate an anonymous class of the License trait + def license : License } + trait Address { def line1 : String def line2 : String @@ -36,8 +37,8 @@ object Branches extends SimpleInjector { trait Branch { def branchId : BranchId def name : String - //def address : Address - //def meta : Meta = new Meta {} // Note: {} used to instantiate an anonymous class of the Meta trait + def address : Address + def meta : Meta } val branchesProvider = new Inject(buildOne _) {} diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index cf613be94..3af362be9 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -17,9 +17,6 @@ object MappedBranchesProvider extends BranchesProvider { Some(MappedBranch.findAll(By(MappedBranch.mBankId, bankId.value))) } - - - // override protected def branchLicense(bank: BankId): Option[License] = // MappedDataLicense.find(By(MappedDataLicense.mBankId, bank.value)) } @@ -28,39 +25,46 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { override def getSingleton = MappedBranch - - - - object mBankId extends DefaultStringField(this) object mName extends DefaultStringField(this) object mBranchId extends DefaultStringField(this) + // Exposed inside address. See below object mLine1 extends DefaultStringField(this) object mLine2 extends DefaultStringField(this) object mLine3 extends DefaultStringField(this) object mLine4 extends DefaultStringField(this) object mLine5 extends DefaultStringField(this) - object mCountryCode extends MappedString(this, 2) object mPostCode extends DefaultStringField(this) + + // Exposed inside meta.license See below object mLicenseName extends DefaultStringField(this) object mLicenseUrl extends DefaultStringField(this) override def branchId: BranchId = BranchId(mBranchId.get) override def name: String = mName.get -// override def address: Address = new Address { -// override def line1: String = mLine1.get -// override def line2: String = mLine2.get -// override def line3: String = mLine3.get -// override def line4: String = mLine4.get -// override def line5: String = mLine5.get -// override def countryCode: String = mCountryCode.get -// override def postCode: String = mPostCode.get -// } + override def address: Address = new Address { + override def line1: String = mLine1.get + override def line2: String = mLine2.get + override def line3: String = mLine3.get + override def line4: String = mLine4.get + override def line5: String = mLine5.get + override def countryCode: String = mCountryCode.get + override def postCode: String = mPostCode.get + } + + override def meta: Meta = new Meta { + override def license: License = new License { + override def name: String = mLicenseName.get + override def url: String = mLicenseUrl.get + } + } + + } @@ -76,18 +80,18 @@ Else could store a link to this with each open data record - or via config for e */ -class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdPK { - override def getSingleton = MappedLicense - - object mBankId extends DefaultStringField(this) - object mName extends DefaultStringField(this) - object mUrl extends DefaultStringField(this) - - override def name: String = mName.get - override def url: String = mUrl.get -} - - -object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { - override def dbIndexes = Index(mBankId) :: super.dbIndexes -} \ No newline at end of file +//class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdPK { +// override def getSingleton = MappedLicense +// +// object mBankId extends DefaultStringField(this) +// object mName extends DefaultStringField(this) +// object mUrl extends DefaultStringField(this) +// +// override def name: String = mName.get +// override def url: String = mUrl.get +//} +// +// +//object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { +// override def dbIndexes = Index(mBankId) :: super.dbIndexes +//} \ No newline at end of file diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index fc6d3f7e1..e50514551 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -47,7 +47,17 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls .mBranchId(branch.id) .mBankId(branch.bank) .mName(branch.name) - // TODO add the other fields + // Note: address fields are returned in meta.address + // but are stored flat as fields / columns in the table + .mLine1(branch.address.line_1) + .mLine2(branch.address.line_2) + .mLine3(branch.address.line_3) + .mLine4(branch.address.line_4) + .mLine5(branch.address.line_5) + .mPostCode(branch.address.post_code) + .mCountryCode(branch.address.country_code) + .mLicenseName(branch.meta.license.name) + .mLicenseUrl(branch.meta.license.url) }) val validationErrors = mappedBranches.flatMap(_.validate) diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 4a352fbe6..7eefdf543 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -480,13 +480,24 @@ case class SandboxBranchImport( bank: String, name : String, address : SandboxAddressImport, - location : SandboxLocationImport) + location : SandboxLocationImport, + meta : SandboxMetaImport) -case class SandboxDataLicenseImport( - id : String, - bank : String, - name : String, - url : String) +case class SandboxLicenseImport( + name : String, + url : String) + +case class SandboxMetaImport( + license : SandboxLicenseImport +) + + + +//case class SandboxDataLicenseImport( +// id : String, +// bank : String, +// name : String, +// url : String) case class SandboxAddressImport( line_1 : String, @@ -549,5 +560,4 @@ case class SandboxDataImport( users : List[SandboxUserImport], accounts : List[SandboxAccountImport], transactions : List[SandboxTransactionImport], - branches: List[SandboxBranchImport], - licenses: List[SandboxDataLicenseImport]) \ No newline at end of file + branches: List[SandboxBranchImport]) \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index 8ebde792e..b9383787a 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -2,7 +2,7 @@ package code.api.v1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchesJson} import dispatch._ -import code.branches.Branches.{BranchId, Branch, License, Address} +import code.branches.Branches.{BranchId, Branch, License, Address, Meta} import code.branches.{Branches, BranchesProvider} import code.model.BankId import code.util.Helper.prettyJson @@ -13,7 +13,8 @@ class BranchesTest extends V140ServerSetup { val BankWithLicense = BankId("bank-with-license") val BankWithoutLicense = BankId("bank-without-license") - case class BranchImpl(branchId : BranchId, name : String) extends Branch + // Have to repeat the constructor parameters from the trait + case class BranchImpl(branchId : BranchId, name : String, address : Address, meta : Meta) extends Branch case class AddressImpl(line1 : String, line2 : String, line3 : String, line4 : String, line5 : String, postCode : String, countryCode : String) extends Address @@ -21,16 +22,22 @@ class BranchesTest extends V140ServerSetup { val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") val fakeAddress2 = fakeAddress1.copy(line1 = "00000") - val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1") - val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2") + val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1, fakeMeta) + val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2, fakeMeta) - val fakeLicense = new License { - override def name: String = "sample-license" - override def url: String = "http://example.com/license" + val fakeMeta = new Meta { + val license = new License { + override def name: String = "sample-license" + override def url: String = "http://example.com/license" + } } + + + + val mockConnector = new BranchesProvider { override protected def getBranchesFromProvider(bank: BankId): Option[List[Branch]] = { bank match { @@ -64,13 +71,13 @@ class BranchesTest extends V140ServerSetup { def verifySameData(branch: Branch, branchJson : BranchJson) = { branch.name should equal (branchJson.name) branch.branchId should equal(BranchId(branchJson.id)) -// branch.address.line1 should equal(branchJson.address.line_1) -// branch.address.line2 should equal(branchJson.address.line_2) -// branch.address.line3 should equal(branchJson.address.line_3) -// branch.address.line4 should equal(branchJson.address.line_4) -// branch.address.line5 should equal(branchJson.address.line_5) -// branch.address.countryCode should equal(branchJson.address.country) -// branch.address.postCode should equal(branchJson.address.postcode_zip) + branch.address.line1 should equal(branchJson.address.line_1) + branch.address.line2 should equal(branchJson.address.line_2) + branch.address.line3 should equal(branchJson.address.line_3) + branch.address.line4 should equal(branchJson.address.line_4) + branch.address.line5 should equal(branchJson.address.line_5) + branch.address.countryCode should equal(branchJson.address.country) + branch.address.postCode should equal(branchJson.address.postcode) } override def beforeAll() { diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index b2e147c04..4b770a36f 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -8,7 +8,7 @@ class MappedBranchesProviderTest extends ServerSetup { private def delete(): Unit = { MappedBranch.bulkDelete_!!() - MappedLicense.bulkDelete_!!() + //MappedLicense.bulkDelete_!!() } override def beforeAll() = { @@ -26,10 +26,10 @@ class MappedBranchesProviderTest extends ServerSetup { val bankIdWithLicenseAndData = "some-bank" val bankIdWithNoLicense = "unlicensed-bank" - val license = MappedLicense.create - .mBankId(bankIdWithLicenseAndData) - .mName("some-license") - .mUrl("http://www.example.com/license").saveMe() +// val license = MappedLicense.create +// .mBankId(bankIdWithLicenseAndData) +// .mName("some-license") +// .mUrl("http://www.example.com/license").saveMe() val unlicensedBranch = MappedBranch.create .mBankId(bankIdWithNoLicense) @@ -53,7 +53,9 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine2("b") .mLine3("c") .mLine4("d") - .mLine5("e").saveMe() + .mLine5("e") + .mLicenseName("some-license") + .mLicenseUrl("http://www.example.com/license").saveMe() val branch2 = MappedBranch.create .mBankId(bankIdWithLicenseAndData) @@ -65,16 +67,18 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine2("b2") .mLine3("c2") .mLine4("d2") - .mLine5("e2").saveMe() + .mLine5("e2") + .mLicenseName("some-license") + .mLicenseUrl("http://www.example.com/license").saveMe() } - feature("MappedBranchesProvider") { + feature("FIX ME MappedBranchesProvider") { scenario("We try to get branch data for a bank which does not have a data license set") { val fixture = defaultSetup() Given("The bank in question has no data license") - MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithNoLicense)) should equal(0) + //MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithNoLicense)) should equal(0) And("The bank in question has branches") MappedBranch.find(By(MappedBranch.mBankId, fixture.bankIdWithNoLicense)).isDefined should equal(true) @@ -86,11 +90,11 @@ class MappedBranchesProviderTest extends ServerSetup { branchData should equal(None) } - scenario("We try to get branch data for a bank which does have a data license set") { + scenario("FIX ME We try to get branch data for a bank which does have a data license set") { val fixture = defaultSetup() val expectedBranches = Set(fixture.branch1, fixture.branch2) Given("We have a data license and branches for a bank") - MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithLicenseAndData)) should equal(1) + //MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithLicenseAndData)) should equal(1) MappedBranch.findAll(By(MappedBranch.mBankId, fixture.bankIdWithLicenseAndData)).toSet should equal(expectedBranches) When("We try to get the branch data for that bank") @@ -105,15 +109,15 @@ class MappedBranchesProviderTest extends ServerSetup { branches.toSet should equal(expectedBranches) } - scenario("We try to get branch data for a bank with a data license, but no branches") { + scenario("FIX ME We try to get branch data for a bank with a data license, but no branches") { Given("We have a data license for a bank, but no branches") val bankWithNoBranches = "bank-with-no-branches" - val license = MappedLicense.create - .mBankId(bankWithNoBranches) - .mName("some-license") - .mUrl("http://www.example.com/license").saveMe() +// val license = MappedLicense.create +// .mBankId(bankWithNoBranches) +// .mName("some-license") +// .mUrl("http://www.example.com/license").saveMe() MappedBranch.find(By(MappedBranch.mBankId, bankWithNoBranches)).isDefined should equal(false) diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index dfe9c6111..bd46cca8d 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -84,14 +84,13 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul xs.mkString("[", ",", "]") } - def createImportJson(banks: List[JValue], users: List[JValue], accounts : List[JValue], transactions : List[JValue], branches : List[JValue], licenses : List[JValue]) : String = { + def createImportJson(banks: List[JValue], users: List[JValue], accounts : List[JValue], transactions : List[JValue], branches : List[JValue]) : String = { val json = ("banks" -> banks) ~ ("users" -> users) ~ ("accounts" -> accounts) ~ ("transactions" -> transactions) ~ - ("branches" -> branches) ~ - ("licenses" -> licenses) + ("branches" -> branches) compact(render(json)) } @@ -139,13 +138,21 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val foundBranchOpt: Option[Branch] = Branches.branchesProvider.vend.getBranch(branchId) foundBranchOpt.isDefined should equal(true) + val foundBranch = foundBranchOpt.get + foundBranch.name should equal(branch.name) + foundBranch.address.line1 should equal(branch.address.line_1) + foundBranch.address.line2 should equal(branch.address.line_2) + foundBranch.address.line3 should equal(branch.address.line_3) + foundBranch.address.line4 should equal(branch.address.line_4) + foundBranch.address.line5 should equal(branch.address.line_5) - // Check some fields - foundBranchOpt.map(_.name).get should equal(branch.name) - // foundBranch.branch.map(_.address).get should equal(branch.address) + foundBranch.address.postCode should equal(branch.address.post_code) + foundBranch.address.countryCode should equal(branch.address.country_code) - // TODO check more branch fields - // TODO check license (need separate get function) + println("yo") + + foundBranch.meta.license.name should equal(branch.meta.license.name) + foundBranch.meta.license.url should equal(branch.meta.license.url) } @@ -301,12 +308,15 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardLocation1 = SandboxLocationImport(52.556198, 13.384099) - val license1AtBank1 = SandboxDataLicenseImport (id = "pddl", bank = bank1.id, name = "PDDL", url = "http://opendatacommons.org/licenses/pddl/") - val standardLicenses = license1AtBank1 :: Nil + //val license1AtBank1 = SandboxDataLicenseImport (id = "pddl", bank = bank1.id, name = "PDDL", url = "http://opendatacommons.org/licenses/pddl/") + //val standardLicenses = license1AtBank1 :: Nil - val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1) - val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1) + val standardLicense = SandboxLicenseImport (name = "PDDL", url = "http://opendatacommons.org/licenses/pddl/") + val standardMeta = SandboxMetaImport (license = standardLicense) + + val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) + val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) val standardBranches = branch1AtBank1 :: branch2AtBank1 :: Nil @@ -410,10 +420,9 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val accounts = standardAccounts val transactions = anotherTransaction :: blankCounterpartyNameTransaction :: blankCounterpartyAccountNumberTransaction :: standardTransactions val branches = standardBranches - val licenses = standardLicenses - val importJson = SandboxDataImport(banks, users, accounts, transactions, branches, licenses) + val importJson = SandboxDataImport(banks, users, accounts, transactions, branches) val response = postImportJson(write(importJson)) response.code should equal(SUCCESS) @@ -425,7 +434,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul } it should "not allow data to be imported without a secret token" in { - val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches, standardLicenses) + val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches) val response = postImportJson(write(importJson), None) response.code should equal(403) @@ -435,7 +444,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul } it should "not allow data to be imported with an invalid secret token" in { - val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches, standardLicenses) + val importJson = SandboxDataImport(standardBanks, standardUsers, standardAccounts, standardTransactions, standardBranches) val badToken = "12345" badToken should not equal(theImportToken) val response = postImportJson(write(importJson), Some(badToken)) @@ -456,7 +465,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankWithoutId = removeIdField(bank1Json) def getResponse(bankJson : JValue) = { - val json = createImportJson(List(bankJson), Nil, Nil, Nil, Nil, Nil) + val json = createImportJson(List(bankJson), Nil, Nil, Nil, Nil) postImportJson(json) } @@ -505,7 +514,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val bankWithSameId = addIdField(baseOtherBank, bank1.id) def getResponse(bankJsons : List[JValue]) = { - val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil, Nil) + val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -523,7 +532,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified bank already exists" in { def getResponse(bankJsons : List[JValue]) = { - val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil, Nil) + val json = createImportJson(bankJsons, Nil, Nil, Nil, Nil) postImportJson(json) } @@ -544,7 +553,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "require users to have valid emails" in { def getResponse(userJson : JValue) = { - val json = createImportJson(Nil, List(userJson), Nil, Nil, Nil, Nil) + val json = createImportJson(Nil, List(userJson), Nil, Nil, Nil) postImportJson(json) } @@ -590,7 +599,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "not allow multiple users with the same email" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -636,7 +645,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified user already exists" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -656,7 +665,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a user's password is missing or empty" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -679,7 +688,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "set user passwords properly" in { def getResponse(userJsons : List[JValue]) = { - val json = createImportJson(Nil, userJsons, Nil, Nil, Nil, Nil) + val json = createImportJson(Nil, userJsons, Nil, Nil, Nil) postImportJson(json) } @@ -699,7 +708,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -726,7 +735,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -755,7 +764,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(accountJsons : List[JValue]) = { val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } val account1AtBank1Json = Extraction.decompose(account1AtBank1) @@ -780,7 +789,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -797,7 +806,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks.map(Extraction.decompose) val users = standardUsers.map(Extraction.decompose) - val json = createImportJson(banks, users, accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks, users, accountJsons, Nil, Nil) postImportJson(json) } @@ -818,7 +827,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks def getResponse(accountJsons : List[JValue]) = { - val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil) postImportJson(json) } @@ -846,7 +855,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val banks = standardBanks def getResponse(accountJsons : List[JValue]) = { - val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil, Nil) + val json = createImportJson(banks.map(Extraction.decompose), users.map(Extraction.decompose), accountJsons, Nil, Nil) postImportJson(json) } @@ -875,7 +884,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -906,7 +915,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -947,7 +956,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul it should "fail if a specified transaction already exists" in { def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(standardBanks.map(Extraction.decompose), - standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + standardUsers.map(Extraction.decompose), standardAccounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -974,7 +983,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1029,7 +1038,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1057,7 +1066,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1085,7 +1094,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1112,7 +1121,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1139,7 +1148,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1173,7 +1182,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1206,7 +1215,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1242,7 +1251,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1276,7 +1285,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1320,7 +1329,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1365,7 +1374,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1418,7 +1427,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1470,7 +1479,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul def getResponse(transactionJsons : List[JValue]) = { val json = createImportJson(banks.map(Extraction.decompose), - users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil, Nil) + users.map(Extraction.decompose), accounts.map(Extraction.decompose), transactionJsons, Nil) postImportJson(json) } @@ -1501,8 +1510,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul // Helper function expects banks and branches def getResponse(branchList : List[JValue]) = { - val json = createImportJson(standardBanks.map(Extraction.decompose), Nil, Nil, Nil, branchList, - standardLicenses.map(Extraction.decompose)) + val json = createImportJson(standardBanks.map(Extraction.decompose), Nil, Nil, Nil, branchList) println(json) postImportJson(json) } @@ -1528,9 +1536,6 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val countBranchesAfter = countOfBranches(Branches.branchesProvider.vend.getBranches(bankId1)) countBranchesAfter should equal(standardBranches.size) // We expect N branches - // TODO Check license situation - //standardBranches.foreach(verifyLicenseCreated) - // Check that for each branch we did indeed create something good standardBranches.foreach(verifyBranchCreated) } From a27e0a162a66b51e988a8fe591f3a79cb55dedac Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 1 May 2015 16:38:49 +0200 Subject: [PATCH 051/702] Branches json containing meta.licence --- .../scala/code/api/v1_4_0/APIMethods140.scala | 3 +++ .../code/api/v1_4_0/JSONFactory1_4_0.scala | 26 ++++++++++++------- src/main/scala/code/branches/Branches.scala | 2 -- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 0a83ecd89..077d5dc7a 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -71,9 +71,12 @@ trait APIMethods140 { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { for { + // Get branches from the active provider branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available. License may not be set.", 404) } yield { + // Format the data as json val json = JSONFactory1_4_0.createBranchesJson(branches) + // Return successJsonResponse(Extraction.decompose(json)) } } diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 2a2f54650..e4d9702a7 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -3,7 +3,7 @@ package code.api.v1_4_0 import java.util.Date import code.branches.Branches -import code.branches.Branches.{Branch, Meta} +import code.branches.Branches.{Branch, Meta, License} import code.customerinfo.{CustomerMessage, CustomerInfo} object JSONFactory1_4_0 { @@ -21,11 +21,12 @@ object JSONFactory1_4_0 { case class AddCustomerMessageJson(message : String, from_department : String, from_person : String) - //case class BranchDataJson(license : DataLicenseJson, branches : List[BranchJson]) - case class DataLicenseJson(name : String, url : String) + case class LicenseJson(name : String, url : String) + + case class MetaJson(license : LicenseJson) - case class BranchJson(id : String, name : String, address : AddressJson, meta : Meta) + case class BranchJson(id : String, name : String, address : AddressJson, meta : MetaJson) case class BranchesJson (branches : List[BranchJson]) case class AddressJson(line_1 : String, line_2 : String, line_3 : String, line_4 : String, line_5 : String, postcode : String, country : String) @@ -47,17 +48,24 @@ object JSONFactory1_4_0 { def createCustomerMessagesJson(messages : List[CustomerMessage]) : CustomerMessagesJson = { CustomerMessagesJson(messages.map(createCustomerMessageJson)) } -// -// def createDataLicenseJson(dataLicense : DataLicense) : DataLicenseJson = { -// DataLicenseJson(dataLicense.name, dataLicense.url) -// } + // Accept a license object and return its json representation + def createLicenseJson(license : License) : LicenseJson = { + LicenseJson(license.name, license.url) + } + + def createMetaJson(meta: Meta) : MetaJson = { + MetaJson(createLicenseJson(meta.license)) + } + + + // Accept an address object and return its json representation def createAddressJson(address : Branches.Address) : AddressJson = { AddressJson(address.line1, address.line2, address.line3, address.line4, address.line5, address.postCode, address.countryCode) } def createBranchJson(branch: Branch) : BranchJson = { - BranchJson(branch.branchId.value, branch.name, createAddressJson(branch.address), branch.meta) + BranchJson(branch.branchId.value, branch.name, createAddressJson(branch.address), createMetaJson(branch.meta)) } def createBranchesJson(branchesList: List[Branch]) : BranchesJson = { diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 5b8ff3739..d57314677 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -72,9 +72,7 @@ trait BranchesProvider { } /* - Return one Branch - Needs bankId so we can get the licence related to the bank (BranchId should be unique anyway) */ final def getBranch(branchId : BranchId) : Option[Branch] = { // // Only return the data if we have a license! From 401f667ad92efbe5c607f47a9887c72b51479d90 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 1 May 2015 19:46:51 +0200 Subject: [PATCH 052/702] Only return Branches that have license set --- src/main/scala/code/branches/Branches.scala | 17 ++-- .../branches/MappedBranchesProviderTest.scala | 93 ++++++++----------- 2 files changed, 46 insertions(+), 64 deletions(-) diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index d57314677..7656ddf05 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -68,22 +68,19 @@ trait BranchesProvider { Implementation details in branchesData */ final def getBranches(bankId : BankId) : Option[List[Branch]] = { - getBranchesFromProvider(bankId) + // If we get branches filter them + getBranchesFromProvider(bankId) match { + case Some(b) => Option(b.filter(x => x.meta.license.name != "" && x.meta.license.url != "")) + case None => None + } } /* Return one Branch */ final def getBranch(branchId : BranchId) : Option[Branch] = { -// // Only return the data if we have a license! -// branchDataLicense(bankId) match { -// case Some(license) => - getBranchFromProvider(branchId) -// case None => { -// logger.warn(s"getBranch says: No branch data license found for bank ${bankId.value}") -// None -// } - + // Filter out if no license data + getBranchFromProvider(branchId).filter(x => x.meta.license.name != "" && x.meta.license.url != "") } protected def getBranchFromProvider(branchId : BranchId) : Option[Branch] diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index 4b770a36f..56176e518 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -3,12 +3,12 @@ package code.branches import code.api.test.ServerSetup import code.model.BankId import net.liftweb.mapper.By +import code.branches.Branches.Branch class MappedBranchesProviderTest extends ServerSetup { private def delete(): Unit = { MappedBranch.bulkDelete_!!() - //MappedLicense.bulkDelete_!!() } override def beforeAll() = { @@ -23,16 +23,13 @@ class MappedBranchesProviderTest extends ServerSetup { def defaultSetup() = new { - val bankIdWithLicenseAndData = "some-bank" - val bankIdWithNoLicense = "unlicensed-bank" + val bankIdX = "some-bank-x" + val bankIdY = "some-bank-y" -// val license = MappedLicense.create -// .mBankId(bankIdWithLicenseAndData) -// .mName("some-license") -// .mUrl("http://www.example.com/license").saveMe() + // 3 branches for bank X (one branch does not have a license) val unlicensedBranch = MappedBranch.create - .mBankId(bankIdWithNoLicense) + .mBankId(bankIdX) .mName("unlicensed") .mBranchId("unlicensed") .mCountryCode("es") @@ -42,9 +39,10 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine3("c4") .mLine4("d4") .mLine5("e4").saveMe() + // Note: The license is not set val branch1 = MappedBranch.create - .mBankId(bankIdWithLicenseAndData) + .mBankId(bankIdX) .mName("branch 1") .mBranchId("branch1") .mCountryCode("de") @@ -58,7 +56,7 @@ class MappedBranchesProviderTest extends ServerSetup { .mLicenseUrl("http://www.example.com/license").saveMe() val branch2 = MappedBranch.create - .mBankId(bankIdWithLicenseAndData) + .mBankId(bankIdX) .mName("branch 2") .mBranchId("branch2") .mCountryCode("fr") @@ -70,66 +68,53 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine5("e2") .mLicenseName("some-license") .mLicenseUrl("http://www.example.com/license").saveMe() + } - feature("FIX ME MappedBranchesProvider") { - scenario("We try to get branch data for a bank which does not have a data license set") { + feature("MappedBranchesProvider") { + + scenario("We try to get branches") { + val fixture = defaultSetup() - Given("The bank in question has no data license") - //MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithNoLicense)) should equal(0) + // Only these have license set + val expectedBranches = Set(fixture.branch1 :: fixture.branch2 :: Nil) - And("The bank in question has branches") - MappedBranch.find(By(MappedBranch.mBankId, fixture.bankIdWithNoLicense)).isDefined should equal(true) - When("We try to get the branch data for that bank") - val branchData = MappedBranchesProvider.getBranches(BankId(fixture.bankIdWithNoLicense)) + Given("the bank in question has branches") + MappedBranch.find(By(MappedBranch.mBankId, fixture.bankIdX)).isDefined should equal(true) - Then("We should get an empty option") - branchData should equal(None) + When("we try to get the branches for that bank") + val branchesOpt: Option[List[Branch]] = MappedBranchesProvider.getBranches(BankId(fixture.bankIdX)) + + Then("We should get a branches list") + branchesOpt.isDefined should equal (true) + val branches = branchesOpt.get + + And("it should contain two branches") + branches.size should equal(2) + + //And("they should be the licensed ones") + //branches.toSet should equal (expectedBranches) } - scenario("FIX ME We try to get branch data for a bank which does have a data license set") { + scenario("We try to get branches for a bank that doesn't have any") { + val fixture = defaultSetup() - val expectedBranches = Set(fixture.branch1, fixture.branch2) - Given("We have a data license and branches for a bank") - //MappedLicense.count(By(MappedLicense.mBankId, fixture.bankIdWithLicenseAndData)) should equal(1) - MappedBranch.findAll(By(MappedBranch.mBankId, fixture.bankIdWithLicenseAndData)).toSet should equal(expectedBranches) - When("We try to get the branch data for that bank") - val branchDataOpt = MappedBranchesProvider.getBranches(BankId(fixture.bankIdWithLicenseAndData)) + Given("we don't have any branches") - Then("We should get back the data license and the branches") + MappedBranch.find(By(MappedBranch.mBankId, fixture.bankIdY)).isDefined should equal(false) + + When("we try to get the branches for that bank") + val branchDataOpt = MappedBranchesProvider.getBranches(BankId(fixture.bankIdY)) + + Then("we should get back an empty list") branchDataOpt.isDefined should equal(true) val branches = branchDataOpt.get - // We no longer have one license per collection so this does not make sense - // branchData.branches. should equal(fixture.license) - branches.toSet should equal(expectedBranches) - } - - scenario("FIX ME We try to get branch data for a bank with a data license, but no branches") { - - Given("We have a data license for a bank, but no branches") - - val bankWithNoBranches = "bank-with-no-branches" -// val license = MappedLicense.create -// .mBankId(bankWithNoBranches) -// .mName("some-license") -// .mUrl("http://www.example.com/license").saveMe() - - MappedBranch.find(By(MappedBranch.mBankId, bankWithNoBranches)).isDefined should equal(false) - - When("We try to get the branch data for that bank") - val branchDataOpt = MappedBranchesProvider.getBranches(BankId(bankWithNoBranches)) - - Then("We should get back the data license, and a list branches of size 0") - branchDataOpt.isDefined should equal(true) - val branches = branchDataOpt.get - - // ditto above: branchData.license should equal(license) - branches should equal(Nil) + branches.size should equal(0) } From 861b11e361f9f877031bea59b4e49767d6fc08cb Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 3 May 2015 15:36:51 +0200 Subject: [PATCH 053/702] Improve branches test --- .../scala/code/branches/MappedBranchesProviderTest.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index 56176e518..6ef9abac5 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -79,7 +79,7 @@ class MappedBranchesProviderTest extends ServerSetup { val fixture = defaultSetup() // Only these have license set - val expectedBranches = Set(fixture.branch1 :: fixture.branch2 :: Nil) + val expectedBranches = List(fixture.branch1, fixture.branch2) Given("the bank in question has branches") @@ -95,8 +95,8 @@ class MappedBranchesProviderTest extends ServerSetup { And("it should contain two branches") branches.size should equal(2) - //And("they should be the licensed ones") - //branches.toSet should equal (expectedBranches) + And("they should be the licensed ones") + branches should equal (expectedBranches) } scenario("We try to get branches for a bank that doesn't have any") { From 46f54a916156d845135c320e1d6941f2b3775fc4 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 5 May 2015 13:22:43 +0200 Subject: [PATCH 054/702] Improving branches tests --- src/main/scala/code/branches/Branches.scala | 10 +++- .../scala/code/api/v1_4_0/BranchesTest.scala | 48 +++++++------------ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 7656ddf05..5510bc5b5 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -70,7 +70,15 @@ trait BranchesProvider { final def getBranches(bankId : BankId) : Option[List[Branch]] = { // If we get branches filter them getBranchesFromProvider(bankId) match { - case Some(b) => Option(b.filter(x => x.meta.license.name != "" && x.meta.license.url != "")) + case Some(branches) => { + + val branchesWithLicense = for { + branch <- branches if branch.meta.license.name.size > 3 && branch.meta.license.name.size > 3 + } yield branch + + //Option(b.filter(x => x.meta.license.name != "" && x.meta.license.url != "")) + Option(branchesWithLicense) + } case None => None } } diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index b9383787a..9864f6294 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -22,11 +22,6 @@ class BranchesTest extends V140ServerSetup { val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") val fakeAddress2 = fakeAddress1.copy(line1 = "00000") - val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1, fakeMeta) - val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2, fakeMeta) - - - val fakeMeta = new Meta { val license = new License { override def name: String = "sample-license" @@ -34,38 +29,38 @@ class BranchesTest extends V140ServerSetup { } } + val fakeMetaNoLicense = new Meta { + val license = new License { + override def name: String = "" + override def url: String = "" + } + } + val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1, fakeMeta) + val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2, fakeMeta) + val fakeBranch3 = BranchImpl(BranchId("branch3"), "Branch 3", fakeAddress2, fakeMetaNoLicense) // Should not be returned - + // Note: This mock provider is returning same branches for the fake banks val mockConnector = new BranchesProvider { override protected def getBranchesFromProvider(bank: BankId): Option[List[Branch]] = { + println("heelo from mockConnector getBranchesFromProvider") bank match { - // have it return branches even for the bank without a license so we can test the connector does not return them - case BankWithLicense | BankWithoutLicense=> Some(List(fakeBranch1, fakeBranch2)) + // have it return branches even for the bank without a license so we can test the API does not return them + case BankWithLicense | BankWithoutLicense=> Some(List(fakeBranch1, fakeBranch2, fakeBranch3)) case _ => None } } + // Mock a badly behaving connector that returns data that doesn't have license. override protected def getBranchFromProvider(branchId: BranchId): Option[Branch] = { branchId match { - // have it return a branch even for the bank without a license so we can test the connector does not return them - case BankWithLicense | BankWithoutLicense=> Some(fakeBranch1) + case BankWithLicense => Some(fakeBranch1) + case BankWithoutLicense=> Some(fakeBranch3) // In case the connector returns, the API should guard case _ => None } } - - - - - -// override protected def branchDataLicense(bank: BankId): Option[License] = { -// bank match { -// case BankWithLicense => Some(fakeLicense) -// case _ => None -// } -// } } def verifySameData(branch: Branch, branchJson : BranchJson) = { @@ -100,8 +95,8 @@ class BranchesTest extends V140ServerSetup { val request = (v1_4Request / "banks" / BankWithoutLicense.value / "branches").GET val response = makeGetRequest(request) - Then("We should get a 404") - response.code should equal(404) + Then("We should get a 200") + response.code should equal(200) } scenario("We try to get bank branches for a bank with a data license for branch information") { @@ -117,15 +112,8 @@ class BranchesTest extends V140ServerSetup { val responseBodyOpt = wholeResponseBody.extractOpt[BranchesJson] responseBodyOpt.isDefined should equal(true) - val responseBody = responseBodyOpt.get - //And("We should get the right license") - // TODO put back in -// val license = responseBody.meta.license -// license.name should equal(fakeLicense.name) -// license.url should equal(fakeLicense.url) - And("We should get the right branches") val branches = responseBody.branches From e65626bb95ebc2d1ba2e0f9c74b4f9ade52f2dd8 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 10 May 2015 20:32:17 +0200 Subject: [PATCH 055/702] Using city and state in Address --- .../scala/code/api/v1_4_0/JSONFactory1_4_0.scala | 4 ++-- src/main/scala/code/branches/Branches.scala | 4 ++-- .../scala/code/branches/MappedBranchesProvider.scala | 8 ++++---- .../sandbox/LocalMappedConnectorDataImport.scala | 4 ++-- src/main/scala/code/sandbox/OBPDataImport.scala | 4 ++-- src/test/scala/code/api/v1_4_0/BranchesTest.scala | 8 ++++---- .../code/branches/MappedBranchesProviderTest.scala | 12 ++++++------ .../scala/code/sandbox/SandboxDataLoadingTest.scala | 6 +++--- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index e4d9702a7..9955cefcc 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -29,7 +29,7 @@ object JSONFactory1_4_0 { case class BranchJson(id : String, name : String, address : AddressJson, meta : MetaJson) case class BranchesJson (branches : List[BranchJson]) - case class AddressJson(line_1 : String, line_2 : String, line_3 : String, line_4 : String, line_5 : String, postcode : String, country : String) + case class AddressJson(line_1 : String, line_2 : String, line_3 : String, city : String, state : String, postcode : String, country : String) def createCustomerInfoJson(cInfo : CustomerInfo) : CustomerInfoJson = { @@ -61,7 +61,7 @@ object JSONFactory1_4_0 { // Accept an address object and return its json representation def createAddressJson(address : Branches.Address) : AddressJson = { - AddressJson(address.line1, address.line2, address.line3, address.line4, address.line5, address.postCode, address.countryCode) + AddressJson(address.line1, address.line2, address.line3, address.city, address.state, address.postCode, address.countryCode) } def createBranchJson(branch: Branch) : BranchJson = { diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 5510bc5b5..0aad9d562 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -27,8 +27,8 @@ object Branches extends SimpleInjector { def line1 : String def line2 : String def line3 : String - def line4 : String - def line5 : String + def city : String + def state : String // or county def postCode : String //ISO_3166-1_alpha-2 def countryCode : String diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index 3af362be9..30d1d79c3 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -34,8 +34,8 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { object mLine1 extends DefaultStringField(this) object mLine2 extends DefaultStringField(this) object mLine3 extends DefaultStringField(this) - object mLine4 extends DefaultStringField(this) - object mLine5 extends DefaultStringField(this) + object mCity extends DefaultStringField(this) + object mState extends DefaultStringField(this) object mCountryCode extends MappedString(this, 2) object mPostCode extends DefaultStringField(this) @@ -51,8 +51,8 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { override def line1: String = mLine1.get override def line2: String = mLine2.get override def line3: String = mLine3.get - override def line4: String = mLine4.get - override def line5: String = mLine5.get + override def city: String = mCity.get + override def state: String = mState.get override def countryCode: String = mCountryCode.get override def postCode: String = mPostCode.get } diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index e50514551..97e0da0a8 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -52,8 +52,8 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls .mLine1(branch.address.line_1) .mLine2(branch.address.line_2) .mLine3(branch.address.line_3) - .mLine4(branch.address.line_4) - .mLine5(branch.address.line_5) + .mCity(branch.address.city) + .mState(branch.address.state) .mPostCode(branch.address.post_code) .mCountryCode(branch.address.country_code) .mLicenseName(branch.meta.license.name) diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 7eefdf543..44096ce10 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -503,8 +503,8 @@ case class SandboxAddressImport( line_1 : String, line_2 : String, line_3 : String, - line_4 : String, - line_5 : String, + city : String, + state : String, // or county / region post_code : String, country_code: String) diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index 9864f6294..59736f8e6 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -15,8 +15,8 @@ class BranchesTest extends V140ServerSetup { // Have to repeat the constructor parameters from the trait case class BranchImpl(branchId : BranchId, name : String, address : Address, meta : Meta) extends Branch - case class AddressImpl(line1 : String, line2 : String, line3 : String, line4 : String, - line5 : String, postCode : String, countryCode : String) extends Address + case class AddressImpl(line1 : String, line2 : String, line3 : String, city : String, + state : String, postCode : String, countryCode : String) extends Address val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") @@ -69,8 +69,8 @@ class BranchesTest extends V140ServerSetup { branch.address.line1 should equal(branchJson.address.line_1) branch.address.line2 should equal(branchJson.address.line_2) branch.address.line3 should equal(branchJson.address.line_3) - branch.address.line4 should equal(branchJson.address.line_4) - branch.address.line5 should equal(branchJson.address.line_5) + branch.address.city should equal(branchJson.address.city) + branch.address.state should equal(branchJson.address.state) branch.address.countryCode should equal(branchJson.address.country) branch.address.postCode should equal(branchJson.address.postcode) } diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index 6ef9abac5..5c03a586e 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -37,8 +37,8 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine1("a4") .mLine2("b4") .mLine3("c4") - .mLine4("d4") - .mLine5("e4").saveMe() + .mCity("d4") + .mState("e4").saveMe() // Note: The license is not set val branch1 = MappedBranch.create @@ -50,8 +50,8 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine1("a") .mLine2("b") .mLine3("c") - .mLine4("d") - .mLine5("e") + .mCity("d") + .mState("e") .mLicenseName("some-license") .mLicenseUrl("http://www.example.com/license").saveMe() @@ -64,8 +64,8 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine1("a2") .mLine2("b2") .mLine3("c2") - .mLine4("d2") - .mLine5("e2") + .mCity("d2") + .mState("e2") .mLicenseName("some-license") .mLicenseUrl("http://www.example.com/license").saveMe() diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index bd46cca8d..cbad40073 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -143,8 +143,8 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul foundBranch.address.line1 should equal(branch.address.line_1) foundBranch.address.line2 should equal(branch.address.line_2) foundBranch.address.line3 should equal(branch.address.line_3) - foundBranch.address.line4 should equal(branch.address.line_4) - foundBranch.address.line5 should equal(branch.address.line_5) + foundBranch.address.city should equal(branch.address.city) + foundBranch.address.state should equal(branch.address.state) foundBranch.address.postCode should equal(branch.address.post_code) foundBranch.address.countryCode should equal(branch.address.country_code) @@ -304,7 +304,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardAddress1 = SandboxAddressImport(line_1 = "5 Some Street", line_2 = "Rosy Place", line_3 = "Sunny Village", - line_4 = "Out There", line_5 = "Derbyshire", post_code = "WHY RU4", country_code = "UK") + city = "Ashbourne", state = "Derbyshire", post_code = "WHY RU4", country_code = "UK") val standardLocation1 = SandboxLocationImport(52.556198, 13.384099) From a59171d7f4d2643c43ef536bce8b1819be551501 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 10 May 2015 21:16:41 +0200 Subject: [PATCH 056/702] Typo in function name. Adding a comment --- src/main/scala/code/api/OBPRestHelper.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 3187eaf61..ed3cc74cf 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -81,15 +81,19 @@ trait OBPRestHelper extends RestHelper with Loggable { def vPlusVersion = "v" + VERSION def apiPrefix = ("obp" / vPlusVersion).oPrefix(_) - implicit def jsonResponseBoxToJsonReponse(box: Box[JsonResponse]): JsonResponse = { + /* + An implicit function to convert magically between a Boxed JsonResponse and a JsonResponse + If we have something good, return it. Else log and return an error. + */ + implicit def jsonResponseBoxToJsonResponse(box: Box[JsonResponse]): JsonResponse = { box match { case Full(r) => r case ParamFailure(_, _, _, apiFailure : APIFailure) => { - logger.info("jsonResponseBoxToJsonReponse case ParamFailure says: API Failure: " + apiFailure.msg + " ($apiFailure.responseCode)") + logger.info("jsonResponseBoxToJsonResponse case ParamFailure says: API Failure: " + apiFailure.msg + " ($apiFailure.responseCode)") errorJsonResponse(apiFailure.msg, apiFailure.responseCode) } case Failure(msg, _, _) => { - logger.info("jsonResponseBoxToJsonReponse case Failure API Failure: " + msg) + logger.info("jsonResponseBoxToJsonResponse case Failure API Failure: " + msg) errorJsonResponse(msg) } case _ => errorJsonResponse() From 22f9e0ee0a9b1b3254132603142f272102c3edf0 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 10 May 2015 21:40:51 +0200 Subject: [PATCH 057/702] import branch json bank -> bank_id so errors more understandable --- src/main/scala/code/api/sandbox/SandboxApiCalls.scala | 2 +- .../scala/code/sandbox/LocalMappedConnectorDataImport.scala | 2 +- src/main/scala/code/sandbox/OBPDataImport.scala | 3 ++- src/test/scala/code/sandbox/SandboxDataLoadingTest.scala | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala index 1c72f2d27..ccabae7b1 100644 --- a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala +++ b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala @@ -15,7 +15,7 @@ object SandboxApiCalls extends OBPRestHelper with Loggable { //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. self: RestHelper => - + logger.debug("Hello from SandboxApiCalls") val VERSION = "sandbox" oauthServe(apiPrefix{ diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 97e0da0a8..48d65b70d 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -45,7 +45,7 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls val mappedBranches = data.map(branch => { MappedBranch.create .mBranchId(branch.id) - .mBankId(branch.bank) + .mBankId(branch.bank_id) .mName(branch.name) // Note: address fields are returned in meta.address // but are stored flat as fields / columns in the table diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 44096ce10..25e1ab394 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -426,6 +426,7 @@ trait OBPDataImport extends Loggable { * @return A full box if the import worked, or else a failure describing what went wrong */ def importData(data: SandboxDataImport) : Box[Unit] = { + logger.debug("Hello from importData") for { banks <- createBanks(data) users <- createUsers(data.users) @@ -477,7 +478,7 @@ case class SandboxBankImport( case class SandboxBranchImport( id : String, - bank: String, + bank_id: String, name : String, address : SandboxAddressImport, location : SandboxLocationImport, diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index cbad40073..184e1270e 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -127,7 +127,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul println("Hello from verifyBranchCreated") // Get ids from input - val bankId = BankId(branch.bank) + val bankId = BankId(branch.bank_id) val branchId = BranchId(branch.id) println(s"bankId is $bankId") @@ -315,8 +315,8 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardLicense = SandboxLicenseImport (name = "PDDL", url = "http://opendatacommons.org/licenses/pddl/") val standardMeta = SandboxMetaImport (license = standardLicense) - val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) - val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) + val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank_id = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) + val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank_id = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) val standardBranches = branch1AtBank1 :: branch2AtBank1 :: Nil From bd72275df251de17e455bddea414691d39f244b1 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 11 May 2015 18:13:43 +0200 Subject: [PATCH 058/702] Adding Address.county Licence id and name instead of name and url --- src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala | 2 +- src/main/scala/code/branches/Branches.scala | 5 +++-- .../scala/code/branches/MappedBranchesProvider.scala | 6 ++++-- .../code/sandbox/LocalMappedConnectorDataImport.scala | 3 ++- src/main/scala/code/sandbox/OBPDataImport.scala | 7 ++++--- src/test/scala/code/api/v1_4_0/BranchesTest.scala | 10 +++++----- .../code/branches/MappedBranchesProviderTest.scala | 8 ++++---- .../scala/code/sandbox/SandboxDataLoadingTest.scala | 9 ++++----- 8 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 9955cefcc..10da90a9f 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -51,7 +51,7 @@ object JSONFactory1_4_0 { // Accept a license object and return its json representation def createLicenseJson(license : License) : LicenseJson = { - LicenseJson(license.name, license.url) + LicenseJson(license.id, license.name) } def createMetaJson(meta: Meta) : MetaJson = { diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 0aad9d562..7297ecf55 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -14,8 +14,8 @@ object Branches extends SimpleInjector { case class BranchId(value : String) trait License { + def id : String def name : String - def url : String } trait Meta { @@ -28,6 +28,7 @@ object Branches extends SimpleInjector { def line2 : String def line3 : String def city : String + def county : String def state : String // or county def postCode : String //ISO_3166-1_alpha-2 @@ -88,7 +89,7 @@ trait BranchesProvider { */ final def getBranch(branchId : BranchId) : Option[Branch] = { // Filter out if no license data - getBranchFromProvider(branchId).filter(x => x.meta.license.name != "" && x.meta.license.url != "") + getBranchFromProvider(branchId).filter(x => x.meta.license.id != "" && x.meta.license.name != "") } protected def getBranchFromProvider(branchId : BranchId) : Option[Branch] diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index 30d1d79c3..9ad4a5e6e 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -35,14 +35,15 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { object mLine2 extends DefaultStringField(this) object mLine3 extends DefaultStringField(this) object mCity extends DefaultStringField(this) + object mCounty extends DefaultStringField(this) object mState extends DefaultStringField(this) object mCountryCode extends MappedString(this, 2) object mPostCode extends DefaultStringField(this) // Exposed inside meta.license See below + object mLicenseId extends DefaultStringField(this) object mLicenseName extends DefaultStringField(this) - object mLicenseUrl extends DefaultStringField(this) override def branchId: BranchId = BranchId(mBranchId.get) override def name: String = mName.get @@ -52,6 +53,7 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { override def line2: String = mLine2.get override def line3: String = mLine3.get override def city: String = mCity.get + override def county: String = mCounty.get override def state: String = mState.get override def countryCode: String = mCountryCode.get override def postCode: String = mPostCode.get @@ -59,8 +61,8 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { override def meta: Meta = new Meta { override def license: License = new License { + override def id: String = mLicenseId.get override def name: String = mLicenseName.get - override def url: String = mLicenseUrl.get } } diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 48d65b70d..74abf8bbb 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -53,11 +53,12 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls .mLine2(branch.address.line_2) .mLine3(branch.address.line_3) .mCity(branch.address.city) + .mCounty(branch.address.county) .mState(branch.address.state) .mPostCode(branch.address.post_code) .mCountryCode(branch.address.country_code) + .mLicenseId(branch.meta.license.id) .mLicenseName(branch.meta.license.name) - .mLicenseUrl(branch.meta.license.url) }) val validationErrors = mappedBranches.flatMap(_.validate) diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 25e1ab394..7443425f0 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -485,8 +485,8 @@ case class SandboxBranchImport( meta : SandboxMetaImport) case class SandboxLicenseImport( - name : String, - url : String) + id : String, + name : String) case class SandboxMetaImport( license : SandboxLicenseImport @@ -505,7 +505,8 @@ case class SandboxAddressImport( line_2 : String, line_3 : String, city : String, - state : String, // or county / region + county : String, // Division of State + state : String, // Division of Country post_code : String, country_code: String) diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index 59736f8e6..df985d843 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -15,24 +15,24 @@ class BranchesTest extends V140ServerSetup { // Have to repeat the constructor parameters from the trait case class BranchImpl(branchId : BranchId, name : String, address : Address, meta : Meta) extends Branch - case class AddressImpl(line1 : String, line2 : String, line3 : String, city : String, + case class AddressImpl(line1 : String, line2 : String, line3 : String, city : String, county : String, state : String, postCode : String, countryCode : String) extends Address - val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "mvmvmv", "C4SF5", "DE") + val fakeAddress1 = AddressImpl("134", "32432", "fff", "fsfsfs", "a county", "mvmvmv", "C4SF5", "DE") val fakeAddress2 = fakeAddress1.copy(line1 = "00000") val fakeMeta = new Meta { val license = new License { - override def name: String = "sample-license" - override def url: String = "http://example.com/license" + override def id: String = "sample-license" + override def name: String = "Sample License" } } val fakeMetaNoLicense = new Meta { val license = new License { + override def id: String = "" override def name: String = "" - override def url: String = "" } } diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index 5c03a586e..e983d0aa1 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -52,8 +52,8 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine3("c") .mCity("d") .mState("e") - .mLicenseName("some-license") - .mLicenseUrl("http://www.example.com/license").saveMe() + .mLicenseId("some-license") + .mLicenseName("Some License").saveMe() val branch2 = MappedBranch.create .mBankId(bankIdX) @@ -66,8 +66,8 @@ class MappedBranchesProviderTest extends ServerSetup { .mLine3("c2") .mCity("d2") .mState("e2") - .mLicenseName("some-license") - .mLicenseUrl("http://www.example.com/license").saveMe() + .mLicenseId("some-license") + .mLicenseName("Some License").saveMe() } diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 184e1270e..b9f2e449c 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -144,15 +144,14 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul foundBranch.address.line2 should equal(branch.address.line_2) foundBranch.address.line3 should equal(branch.address.line_3) foundBranch.address.city should equal(branch.address.city) + foundBranch.address.county should equal(branch.address.county) foundBranch.address.state should equal(branch.address.state) foundBranch.address.postCode should equal(branch.address.post_code) foundBranch.address.countryCode should equal(branch.address.country_code) - println("yo") - + foundBranch.meta.license.id should equal(branch.meta.license.id) foundBranch.meta.license.name should equal(branch.meta.license.name) - foundBranch.meta.license.url should equal(branch.meta.license.url) } @@ -304,7 +303,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardAddress1 = SandboxAddressImport(line_1 = "5 Some Street", line_2 = "Rosy Place", line_3 = "Sunny Village", - city = "Ashbourne", state = "Derbyshire", post_code = "WHY RU4", country_code = "UK") + city = "Ashbourne", county = "Derbyshire", state = "", post_code = "WHY RU4", country_code = "UK") val standardLocation1 = SandboxLocationImport(52.556198, 13.384099) @@ -312,7 +311,7 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul //val standardLicenses = license1AtBank1 :: Nil - val standardLicense = SandboxLicenseImport (name = "PDDL", url = "http://opendatacommons.org/licenses/pddl/") + val standardLicense = SandboxLicenseImport (id = "pddl", name = "Open Data Commons Public Domain Dedication and License (PDDL)") val standardMeta = SandboxMetaImport (license = standardLicense) val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank_id = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) From c61ee8e03be9ae753ce905d20220eb97e2d3c82b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 12 May 2015 09:35:50 +0200 Subject: [PATCH 059/702] Adding lobby and driveUp hours and tests --- src/main/scala/code/branches/Branches.scala | 21 ++++++++++++++++++- .../branches/MappedBranchesProvider.scala | 11 ++++++++++ .../LocalMappedConnectorDataImport.scala | 2 ++ .../scala/code/sandbox/OBPDataImport.scala | 19 +++++++++++------ .../scala/code/api/v1_4_0/BranchesTest.scala | 19 +++++++++++------ .../code/sandbox/SandboxDataLoadingTest.scala | 12 +++++++++-- 6 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 7297ecf55..8486be89d 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -4,6 +4,7 @@ package code.branches import code.branches.Branches.{Branch, BranchId} import code.model.{BankId} +import com.sun.tools.internal.xjc.reader.xmlschema.bindinfo.BIGlobalBinding import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector @@ -29,7 +30,7 @@ object Branches extends SimpleInjector { def line3 : String def city : String def county : String - def state : String // or county + def state : String def postCode : String //ISO_3166-1_alpha-2 def countryCode : String @@ -40,8 +41,26 @@ object Branches extends SimpleInjector { def name : String def address : Address def meta : Meta + def lobby : Lobby + def driveUp : DriveUp } + + trait Lobby { + def hours : String + } + + trait DriveUp { + def hours : String + } + + + + + + + + val branchesProvider = new Inject(buildOne _) {} def buildOne: BranchesProvider = MappedBranchesProvider diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala index 9ad4a5e6e..47129c083 100644 --- a/src/main/scala/code/branches/MappedBranchesProvider.scala +++ b/src/main/scala/code/branches/MappedBranchesProvider.scala @@ -5,6 +5,7 @@ import code.model.BankId import code.util.DefaultStringField import net.liftweb.common.Box import net.liftweb.mapper._ +import org.joda.time.Hours import scala.util.Try @@ -45,6 +46,9 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { object mLicenseId extends DefaultStringField(this) object mLicenseName extends DefaultStringField(this) + object mLobbyHours extends DefaultStringField(this) + object mDriveUpHours extends DefaultStringField(this) + override def branchId: BranchId = BranchId(mBranchId.get) override def name: String = mName.get @@ -66,6 +70,13 @@ class MappedBranch extends Branch with LongKeyedMapper[MappedBranch] with IdPK { } } + override def lobby: Lobby = new Lobby { + override def hours: String = mLobbyHours + } + + override def driveUp: DriveUp = new DriveUp { + override def hours: String = mDriveUpHours + } } diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 74abf8bbb..c93d1a540 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -59,6 +59,8 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls .mCountryCode(branch.address.country_code) .mLicenseId(branch.meta.license.id) .mLicenseName(branch.meta.license.name) + .mLobbyHours(branch.lobby.hours) + .mDriveUpHours(branch.driveUp.hours) }) val validationErrors = mappedBranches.flatMap(_.validate) diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 7443425f0..d86efdb2f 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -426,7 +426,6 @@ trait OBPDataImport extends Loggable { * @return A full box if the import worked, or else a failure describing what went wrong */ def importData(data: SandboxDataImport) : Box[Unit] = { - logger.debug("Hello from importData") for { banks <- createBanks(data) users <- createUsers(data.users) @@ -476,21 +475,30 @@ case class SandboxBankImport( logo : String, website : String) + +// Branches to be imported must match this pattern case class SandboxBranchImport( id : String, bank_id: String, name : String, address : SandboxAddressImport, location : SandboxLocationImport, - meta : SandboxMetaImport) + meta : SandboxMetaImport, + lobby : SandboxLobbyImport, + driveUp : SandboxDriveUpImport) case class SandboxLicenseImport( id : String, name : String) case class SandboxMetaImport( - license : SandboxLicenseImport -) + license : SandboxLicenseImport) + +case class SandboxLobbyImport( + hours : String) + +case class SandboxDriveUpImport( + hours : String) @@ -512,8 +520,7 @@ case class SandboxAddressImport( case class SandboxLocationImport( latitude : Double, - longitude : Double -) + longitude : Double) case class SandboxUserImport( email : String, diff --git a/src/test/scala/code/api/v1_4_0/BranchesTest.scala b/src/test/scala/code/api/v1_4_0/BranchesTest.scala index df985d843..41f9457fe 100644 --- a/src/test/scala/code/api/v1_4_0/BranchesTest.scala +++ b/src/test/scala/code/api/v1_4_0/BranchesTest.scala @@ -2,7 +2,7 @@ package code.api.v1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{BranchJson, BranchesJson} import dispatch._ -import code.branches.Branches.{BranchId, Branch, License, Address, Meta} +import code.branches.Branches.{BranchId, Branch, License, Address, Meta, Lobby, DriveUp} import code.branches.{Branches, BranchesProvider} import code.model.BankId import code.util.Helper.prettyJson @@ -14,7 +14,7 @@ class BranchesTest extends V140ServerSetup { val BankWithoutLicense = BankId("bank-without-license") // Have to repeat the constructor parameters from the trait - case class BranchImpl(branchId : BranchId, name : String, address : Address, meta : Meta) extends Branch + case class BranchImpl(branchId : BranchId, name : String, address : Address, meta : Meta, lobby : Lobby, driveUp : DriveUp) extends Branch case class AddressImpl(line1 : String, line2 : String, line3 : String, city : String, county : String, state : String, postCode : String, countryCode : String) extends Address @@ -37,14 +37,21 @@ class BranchesTest extends V140ServerSetup { } - val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1, fakeMeta) - val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2, fakeMeta) - val fakeBranch3 = BranchImpl(BranchId("branch3"), "Branch 3", fakeAddress2, fakeMetaNoLicense) // Should not be returned + val fakeLobby = new Lobby { + val hours = "M-Th 9-5, Fri 9-6, Sat 9-1" + } + + val fakeDriveUp = new DriveUp { + override def hours: String = "M-Th 8:30 - 5:30, Fri 8:30 - 6, Sat: 9-12" + } + + val fakeBranch1 = BranchImpl(BranchId("branch1"), "Branch 1", fakeAddress1, fakeMeta, fakeLobby, fakeDriveUp) + val fakeBranch2 = BranchImpl(BranchId("branch2"), "Branch 2", fakeAddress2, fakeMeta, fakeLobby, fakeDriveUp) + val fakeBranch3 = BranchImpl(BranchId("branch3"), "Branch 3", fakeAddress2, fakeMetaNoLicense, fakeLobby, fakeDriveUp) // Should not be returned // Note: This mock provider is returning same branches for the fake banks val mockConnector = new BranchesProvider { override protected def getBranchesFromProvider(bank: BankId): Option[List[Branch]] = { - println("heelo from mockConnector getBranchesFromProvider") bank match { // have it return branches even for the bank without a license so we can test the API does not return them case BankWithLicense | BankWithoutLicense=> Some(List(fakeBranch1, fakeBranch2, fakeBranch3)) diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index b9f2e449c..a43923aec 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -153,6 +153,8 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul foundBranch.meta.license.id should equal(branch.meta.license.id) foundBranch.meta.license.name should equal(branch.meta.license.name) + foundBranch.lobby.hours should equal(branch.lobby.hours) + foundBranch.driveUp.hours should equal(branch.driveUp.hours) } @@ -314,8 +316,14 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardLicense = SandboxLicenseImport (id = "pddl", name = "Open Data Commons Public Domain Dedication and License (PDDL)") val standardMeta = SandboxMetaImport (license = standardLicense) - val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank_id = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) - val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank_id = "bank1", address = standardAddress1, location = standardLocation1, meta = standardMeta) + val standardLobby = SandboxLobbyImport(hours = "M-TH 8:30-3:30, F 9-5") + val standardDriveUp = SandboxDriveUpImport(hours = "M-Th 8:30-5:30, F-8:30-6, Sat 9-12") + + + val branch1AtBank1 = SandboxBranchImport(id = "branch1", name = "Ashbourne", bank_id = "bank1", address = standardAddress1 + , location = standardLocation1, meta = standardMeta, lobby = standardLobby, driveUp = standardDriveUp) + val branch2AtBank1 = SandboxBranchImport(id = "branch2", name = "Manchester", bank_id = "bank1", address = standardAddress1 + , location = standardLocation1, meta = standardMeta, lobby = standardLobby, driveUp = standardDriveUp) val standardBranches = branch1AtBank1 :: branch2AtBank1 :: Nil From a8bfba7fb7ed4e6b5c646d0a4223e77c62b39df4 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 12 May 2015 11:15:25 +0200 Subject: [PATCH 060/702] Remove unused import that sneaked in --- src/main/scala/code/branches/Branches.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala index 8486be89d..b867563c4 100644 --- a/src/main/scala/code/branches/Branches.scala +++ b/src/main/scala/code/branches/Branches.scala @@ -4,12 +4,9 @@ package code.branches import code.branches.Branches.{Branch, BranchId} import code.model.{BankId} -import com.sun.tools.internal.xjc.reader.xmlschema.bindinfo.BIGlobalBinding import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector -import scala.util.Try - object Branches extends SimpleInjector { case class BranchId(value : String) From 3a610524930ae71371288eac6e07d78deac96b98 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 12 May 2015 12:48:15 +0200 Subject: [PATCH 061/702] gitComit now logs a warning if it returns "" --- src/main/scala/code/api/OBPAPI1.0.scala | 2 ++ src/main/scala/code/api/util/APIUtil.scala | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/OBPAPI1.0.scala b/src/main/scala/code/api/OBPAPI1.0.scala index 624983d2b..8a7d40d58 100644 --- a/src/main/scala/code/api/OBPAPI1.0.scala +++ b/src/main/scala/code/api/OBPAPI1.0.scala @@ -65,6 +65,8 @@ object OBPAPI1_0 extends RestHelper with Loggable { //log the API call logAPICall + // NOTE: This function has been pulled out to gitCommit in APIUtil.scala + // Not updating this code since its 1.0 def gitCommit : String = { val commit = tryo{ val properties = new java.util.Properties() diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 11b7ecac5..88a1cb448 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -34,7 +34,7 @@ package code.api.util import code.api.v1_2.ErrorMessage import code.metrics.APIMetrics -import net.liftweb.common.Full +import net.liftweb.common.{Full, Loggable} import net.liftweb.http.{JsonResponse, S} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.js.JsExp @@ -44,7 +44,8 @@ import net.liftweb.util.Helpers._ import scala.collection.JavaConversions.asScalaSet -object APIUtil { + +object APIUtil extends Loggable { implicit val formats = net.liftweb.json.DefaultFormats implicit def errorToJson(error: ErrorMessage): JValue = Extraction.decompose(error) @@ -69,13 +70,25 @@ object APIUtil { def logAPICall = APIMetrics.apiMetrics.vend.saveMetric(S.uriAndQueryString.getOrElse(""), (now: TimeSpan)) + + /* + Return the git commit. If we can't for some reason (not a git root etc) then log and return "" + */ def gitCommit : String = { - val commit = tryo{ + val commit = try { val properties = new java.util.Properties() + logger.debug("Before getResourceAsStream git.properties") properties.load(getClass().getClassLoader().getResourceAsStream("git.properties")) + logger.debug("Before get Property git.commit.id") properties.getProperty("git.commit.id", "") + } catch { + case e : Throwable => { + logger.warn("gitCommit says: Could not return git commit. Does resources/git.properties exist?") + logger.error(s"Exception in gitCommit: $e") + "" // Return empty string + } } - commit getOrElse "" + commit } def noContentJsonResponse : JsonResponse = From cea82e0908050de7a63c26eeb5b5b82bf0e36a8f Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Fri, 15 May 2015 20:36:29 +0200 Subject: [PATCH 062/702] only check for lastUpdate of account if value is set (didn't trigger hbci updating) --- src/main/scala/code/bankconnectors/LocalMappedConnector.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index b6d70006e..80f0f6ffd 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -94,7 +94,9 @@ object LocalMappedConnector extends Connector with Loggable { } { spawn{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) - val outDatedTransactions = now after time(account.lastUpdate.get.getTime + hours(1)) + val outDatedTransactions = + if(account.lastUpdate.dbNotNull_?) now after time(account.lastUpdate.get.getTime + hours(1)) + else true if(outDatedTransactions && useMessageQueue) { UpdatesRequestSender.sendMsg(UpdateBankAccount(account.accountNumber.get, bank.national_identifier.get)) } From ad2d9cf58e089876768f515c1927ca849a3ca60d Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Sat, 16 May 2015 12:50:54 +0200 Subject: [PATCH 063/702] transaction importer fixes * properly handle null values * update lastUpdate when updating account data * allow setting interval in props file --- .../resources/props/sample.props.template | 29 +++++++++---------- .../bankconnectors/LocalMappedConnector.scala | 8 +++-- .../scala/code/management/ImporterAPI.scala | 3 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 87e6744f6..c24f86f01 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -23,9 +23,6 @@ hostname=http://127.0.0.1:8080 #if you want to change the port when running via the command line, use "mvn -Djetty.port=8089 jetty:run" instead dev.port=8089 -#set this to false if you don't want the api payments call to work -payments_enabled=true - #mail server config: not need in dev mode, but important for production mail.api.consumer.registered.sender.address=no-reply@example.com mail.api.consumer.registered.notification.addresses=you@example.com @@ -36,13 +33,8 @@ mail.smtp.port=25 #to your own secret key BankMockKey=change_me - -#tesobe specific settings - -#rabbitMQ settings (used to communicate with HBCI project) -connection.host=localhost -connection.user=theusername -connection.password=thepassword +#set this to false if you don't want the api payments call to work +payments_enabled=true #secret key that allows access to the "add cash transactions" api. You should change this to your own secret key @@ -51,12 +43,6 @@ cashApplicationKey=change_me #secret key that allows access to the "add transactions" api. You should change this to your own secret key importer_secret=change_me -#set this to true if you want to have the api to send a message to the hbci project to refresh transactions for an account -messageQueue.updateBankAccountsTransaction=false - -#set this to true if you want to have the api listen for "create account" messages from the hbci project -messageQueue.createBankAccounts=true - #set this to true if you want to allow users to create sandbox test accounts with a starting balance allow_sandbox_account_creation=true @@ -68,6 +54,17 @@ sandbox_data_import_secret=change_me #management modules +#rabbitMQ settings (used to communicate with HBCI project) +connection.host=localhost +connection.user=theusername +connection.password=thepassword + +#set this to true if you want to have the api to send a message to the hbci project to refresh transactions for an account +messageQueue.updateBankAccountsTransaction=false + +#set this to true if you want to have the api listen for "create account" messages from the hbci project +messageQueue.createBankAccounts=true + #set this to true if you want to allow users to delete accounts allow_account_deletion=true diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 80f0f6ffd..63330077e 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -94,9 +94,10 @@ object LocalMappedConnector extends Connector with Loggable { } { spawn{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) - val outDatedTransactions = - if(account.lastUpdate.dbNotNull_?) now after time(account.lastUpdate.get.getTime + hours(1)) - else true + val outDatedTransactions = Box!!account.lastUpdate.get match { + case Full(l) => now after time(l.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1))) + case _ => true + } if(outDatedTransactions && useMessageQueue) { UpdatesRequestSender.sendMsg(UpdateBankAccount(account.accountNumber.get, bank.national_identifier.get)) } @@ -369,6 +370,7 @@ object LocalMappedConnector extends Connector with Loggable { acc <- getBankAccountType(bankId, accountId) } yield { acc.accountBalance(Helper.convertToSmallestCurrencyUnits(newBalance, acc.currency)).save + acc.lastUpdate(now).save; } result.getOrElse(false) diff --git a/src/main/scala/code/management/ImporterAPI.scala b/src/main/scala/code/management/ImporterAPI.scala index ad55bc58a..3692852fa 100644 --- a/src/main/scala/code/management/ImporterAPI.scala +++ b/src/main/scala/code/management/ImporterAPI.scala @@ -107,8 +107,7 @@ object ImporterAPI extends RestHelper with Loggable { case "obp_transactions_saver" :: "api" :: "transactions" :: Nil JsonPost json => { - def savetransactions ={ - + def savetransactions = { def updateBankAccountBalance(insertedTransactions : List[Transaction]) = { if(insertedTransactions.nonEmpty) { //we assume here that all the Envelopes concern only one account From f9723f6ce8bc30d2722137999ee183f920951d8f Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Sat, 16 May 2015 17:04:10 +0200 Subject: [PATCH 064/702] also set lastUpdate when adding duplicate transactions, add tests for setting lastUpdate time --- .../scala/code/bankconnectors/Connector.scala | 6 ++--- .../code/bankconnectors/LocalConnector.scala | 16 ++++++++++--- .../bankconnectors/LocalMappedConnector.scala | 23 ++++++++++++++++--- .../scala/code/management/ImporterAPI.scala | 15 +++++++++++- src/main/scala/code/model/BankingData.scala | 1 + .../scala/code/model/dataAccess/Account.scala | 9 +++----- .../model/dataAccess/MappedBankAccount.scala | 6 ++--- .../code/api/LocalConnectorTestSetup.scala | 2 +- .../code/api/v1_3_0/PhysicalCardsTest.scala | 4 +--- .../scala/code/management/ImporterTest.scala | 20 +++++++++++++++- 10 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index c4eac5102..744e3fcdb 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -150,12 +150,10 @@ trait Connector { def addCashTransactionAndUpdateBalance(account : AccountType, cashTransaction : CashTransaction) //used by transaction import api call to check for duplicates + //the implementation is responsible for dealing with the amount as a string def getMatchingTransactionCount(bankNationalIdentifier : String, accountNumber : String, amount : String, completed : Date, otherAccountHolder : String) : Int - - //used by transaction import api def createImportedTransaction(transaction: ImporterTransaction) : Box[Transaction] - - //used by the transaction import api def updateAccountBalance(bankId : BankId, accountId : AccountId, newBalance : BigDecimal) : Boolean + def setBankAccountLastUpdated(bankNationalIdentifier: String, accountNumber : String, updateDate: Date) : Boolean } \ No newline at end of file diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 228da1271..a6bda2c85 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -295,7 +295,7 @@ private object LocalConnector extends Connector with Loggable { private def updateAccountTransactions(bank: HostedBank, account: Account): Unit = { spawn{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) - val outDatedTransactions = now after time(account.lastUpdate.get.getTime + hours(1)) + val outDatedTransactions = now after time(account.accountLastUpdate.get.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1))) if(outDatedTransactions && useMessageQueue) { UpdatesRequestSender.sendMsg(UpdateBankAccount(account.accountNumber.get, bank.national_identifier.get)) } @@ -350,7 +350,7 @@ private object LocalConnector extends Connector with Loggable { .accountLabel("") .accountCurrency(currency) .accountIban("") - .lastUpdate(now) + .accountLastUpdate(now) .save bankAccount } @@ -500,7 +500,7 @@ private object LocalConnector extends Connector with Loggable { val env = OBPEnvelope.createRecord. obp_transaction(transaction) - account.accountBalance(account.balance + amount).lastUpdate(now) + account.accountBalance(account.balance + amount).accountLastUpdate(now) account.save env.save } @@ -572,4 +572,14 @@ private object LocalConnector extends Connector with Loggable { false } } + + override def setBankAccountLastUpdated(bankNationalIdentifier: String, accountNumber : String, updateDate: Date) : Boolean = { + Account.find( + (Account.accountNumber.name -> accountNumber)~ + (Account.nationalIdentifier.name -> bankNationalIdentifier) + ) match { + case Full(acc) => acc.accountLastUpdate(updateDate).saveTheRecord().isDefined + case _ => logger.warn("can't set bank account.lastUpdated because the account was not found"); false + } + } } \ No newline at end of file diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 63330077e..24d52b86c 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -94,7 +94,7 @@ object LocalMappedConnector extends Connector with Loggable { } { spawn{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) - val outDatedTransactions = Box!!account.lastUpdate.get match { + val outDatedTransactions = Box!!account.accountLastUpdate.get match { case Full(l) => now after time(l.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1))) case _ => true } @@ -368,9 +368,10 @@ object LocalMappedConnector extends Connector with Loggable { //this will be Full(true) if everything went well val result = for { acc <- getBankAccountType(bankId, accountId) + bank <- getMappedBank(bankId) } yield { acc.accountBalance(Helper.convertToSmallestCurrencyUnits(newBalance, acc.currency)).save - acc.lastUpdate(now).save; + setBankAccountLastUpdated(bank.nationalIdentifier, acc.number, now) } result.getOrElse(false) @@ -458,12 +459,28 @@ object LocalMappedConnector extends Connector with Loggable { } yield transaction } + override def setBankAccountLastUpdated(bankNationalIdentifier: String, accountNumber : String, updateDate: Date) : Boolean = { + val result = for { + bankId <- getBankByNationalIdentifier(bankNationalIdentifier).map(_.bankId) + account <- getAccountByNumber(bankId, accountNumber) + } yield { + val acc = MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, account.accountId.value) + ) + acc match { + case Full(a) => a.accountLastUpdate(updateDate).save + case _ => logger.warn("can't set bank account.lastUpdated because the account was not found"); false + } + } + result.getOrElse(false) + } + /* End of transaction importer api */ - /* Cash api */ diff --git a/src/main/scala/code/management/ImporterAPI.scala b/src/main/scala/code/management/ImporterAPI.scala index 3692852fa..b7bc8357e 100644 --- a/src/main/scala/code/management/ImporterAPI.scala +++ b/src/main/scala/code/management/ImporterAPI.scala @@ -3,8 +3,9 @@ package code.management import java.util.Date import code.bankconnectors.Connector -import code.model.Transaction +import code.model.{Transaction, BankId, AccountId} import code.tesobe.ErrorMessage +import code.util.Helper import net.liftweb.common.{Full, Loggable} import net.liftweb.http._ import net.liftweb.http.rest.RestHelper @@ -42,6 +43,12 @@ object ImporterAPI extends RestHelper with Loggable { case class ImporterDate(`$dt` : Date) //format : "2012-01-04T18:06:22.000Z" (see tests) case class ImporterAmount(currency : String, amount : String) + implicit object ImporterDateOrdering extends Ordering[ImporterDate]{ + def compare(x: ImporterDate, y: ImporterDate): Int ={ + x.`$dt`.compareTo(y.`$dt`) + } + } + def errorJsonResponse(message : String = "error", httpCode : Int = 400) : JsonResponse = JsonResponse(Extraction.decompose(ErrorMessage(message)), Nil, Nil, httpCode) @@ -154,6 +161,12 @@ object ImporterAPI extends RestHelper with Loggable { val insertedTs = inserted.l logger.info("inserted " + insertedTs.size + " transactions") updateBankAccountBalance(insertedTs) + if (insertedTs.isEmpty && importerTransactions.nonEmpty) { + //refresh account lastUpdate in case transactions were posted but they were all duplicates (account was still "refreshed") + val mostRecentTransaction = importerTransactions.maxBy(t => t.obp_transaction.details.completed) + val account = mostRecentTransaction.obp_transaction.this_account + Connector.connector.vend.setBankAccountLastUpdated(account.bank.national_identifier, account.number, now) + } val jsonList = insertedTs.map(whenAddedJson) JsonResponse(JArray(jsonList)) case _ => { diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 7c0b52dfa..50bfbd214 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -184,6 +184,7 @@ trait BankAccount { def iban : Option[String] def number : String //the actual number as given by the bank def bankId : BankId //bank identifier, usually short name of + def lastUpdate : Date @deprecated("Get the account holder(s) via owners") def accountHolder : String diff --git a/src/main/scala/code/model/dataAccess/Account.scala b/src/main/scala/code/model/dataAccess/Account.scala index 62ceee5c7..f5411fb1d 100644 --- a/src/main/scala/code/model/dataAccess/Account.scala +++ b/src/main/scala/code/model/dataAccess/Account.scala @@ -32,24 +32,20 @@ Berlin 13359, Germany package code.model.dataAccess +import java.util.Date import com.mongodb.QueryBuilder import net.liftweb.mongodb.record.MongoMetaRecord import net.liftweb.mongodb.record.field.ObjectIdPk -import net.liftweb.mongodb.record.field.ObjectIdRefListField import net.liftweb.mongodb.record.MongoRecord import net.liftweb.mongodb.record.field.ObjectIdRefField import net.liftweb.mongodb.record.field.DateField import net.liftweb.common._ -import net.liftweb.mongodb.record.field.BsonRecordField -import net.liftweb.mongodb.record.BsonRecord import net.liftweb.record.field.{ StringField, BooleanField, DecimalField } import net.liftweb.mongodb.{Limit, Skip} import code.model._ import net.liftweb.mongodb.BsonDSL._ -import OBPEnvelope._ import code.bankconnectors._ import code.bankconnectors.OBPOffset -import scala.Some import code.bankconnectors.OBPLimit import code.bankconnectors.OBPOrdering import net.liftweb.mongodb.Limit @@ -87,7 +83,7 @@ class Account extends BankAccount with MongoRecord[Account] with ObjectIdPk[Acco //this is the legacy db field name override def name = "iban" } - object lastUpdate extends DateField(this) + object accountLastUpdate extends DateField(this) def transactionsForAccount: QueryBuilder = { QueryBuilder @@ -178,6 +174,7 @@ class Account extends BankAccount with MongoRecord[Account] with ObjectIdPk[Acco override def accountType: String = kind.get override def label: String = accountLabel.get override def accountHolder: String = holder.get + override def lastUpdate: Date = accountLastUpdate.get } object Account extends Account with MongoMetaRecord[Account] { diff --git a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala index 5d049f6d3..db0e2a295 100644 --- a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala +++ b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala @@ -1,7 +1,6 @@ package code.model.dataAccess -import java.math.MathContext - +import java.util.Date import code.model._ import code.util.{MappedAccountNumber, MappedUUID, Helper} import net.liftweb.mapper._ @@ -31,7 +30,7 @@ class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccou object accountLabel extends MappedString(this, 255) //the last time this account was updated via hbci - object lastUpdate extends MappedDateTime(this) + object accountLastUpdate extends MappedDateTime(this) override def uuid = accUUID.get override def accountId: AccountId = AccountId(theAccountId.get) @@ -51,6 +50,7 @@ class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccou override def accountType: String = kind.get override def label: String = accountLabel.get override def accountHolder: String = holder.get + override def lastUpdate : Date = accountLastUpdate.get } object MappedBankAccount extends MappedBankAccount with LongKeyedMetaMapper[MappedBankAccount] { diff --git a/src/test/scala/code/api/LocalConnectorTestSetup.scala b/src/test/scala/code/api/LocalConnectorTestSetup.scala index 3b5b3eb8d..600fd70b4 100644 --- a/src/test/scala/code/api/LocalConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalConnectorTestSetup.scala @@ -96,7 +96,7 @@ trait LocalConnectorTestSetup extends TestConnectorSetupWithStandardPermissions obp_transaction(transaction).save //slightly ugly - account.asInstanceOf[Account].accountBalance(newBalance.amount.get).lastUpdate(now).save + account.asInstanceOf[Account].accountBalance(newBalance.amount.get).accountLastUpdate(now).save env.save } diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index ad8ad7d9e..46a11408c 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -120,12 +120,10 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { //used by transaction import api call to check for duplicates override def getMatchingTransactionCount(bankNationalIdentifier : String, accountNumber : String, amount: String, completed: Date, otherAccountHolder: String): Int = ??? - //used by transaction import api override def createImportedTransaction(transaction: ImporterTransaction): Box[Transaction] = ??? - - //used by the transaction import api override def updateAccountBalance(bankId: BankId, accountId: AccountId, newBalance: BigDecimal): Boolean = ??? + override def setBankAccountLastUpdated(bankNationalIdentifier: String, accountNumber : String, updateDate: Date) : Boolean = ??? } override def beforeAll() { diff --git a/src/test/scala/code/management/ImporterTest.scala b/src/test/scala/code/management/ImporterTest.scala index 335853b7a..76ebac962 100644 --- a/src/test/scala/code/management/ImporterTest.scala +++ b/src/test/scala/code/management/ImporterTest.scala @@ -10,6 +10,7 @@ import code.model.{AccountId, Transaction} import dispatch._ import net.liftweb.common.Loggable import net.liftweb.util.Props +import net.liftweb.util.TimeHelpers._ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSetup { @@ -63,7 +64,7 @@ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSe s"""{ | "obp_transaction": { | "this_account": { - | "holder": "Alan Holder", + | "holder": "${account.accountHolder}", | "number": "${account.number}", | "kind": "${account.accountType}", | "bank": { @@ -209,6 +210,10 @@ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSe val account = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get account.balance.toString should equal(f.t2NewBalance) //t2 has a later completed date than t1 + And("The account should have accountLastUpdate set to the current time") + val dt = (now.getTime - account.lastUpdate.getTime) + dt < 1000 should equal(true) + } scenario("Attempting to add 'identical' transactions") { @@ -239,6 +244,10 @@ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSe And("The account should have its balance set to the 'new_balance' value of the most recently completed transaction") val account = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get account.balance.toString should equal(f.t1NewBalance) + + And("The account should have accountLastUpdate set to the current time") + val dt = (now.getTime - account.lastUpdate.getTime) + dt < 1000 should equal(true) } scenario("Adding transactions that have already been imported") { @@ -255,6 +264,10 @@ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSe tsBefore.foreach(checkTransactionOkay) + //remember lastUpdate time + var account = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get + val oldTime = if(account.lastUpdate != null) account.lastUpdate.getTime else 0 + When("We try to add those transactions again") val response = addTransactions(importJson, Some(secretKeyValue)) @@ -266,6 +279,11 @@ class ImporterTest extends ServerSetup with Loggable with DefaultConnectorTestSe tsAfter.size should equal(2) tsAfter.foreach(checkTransactionOkay) + + And("The account should have accountLastUpdate set to the current time (different from first insertion)") + account = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get + val dt = (account.lastUpdate.getTime - oldTime) + dt > 0 should equal(true) } scenario("Adding 'identical' transactions, some of which have already been imported") { From 914675c9307955875fe8baa12fa5b13e1965ec73 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Sat, 16 May 2015 18:48:45 +0200 Subject: [PATCH 065/702] take over some authorize form changes from ulster branch --- src/main/webapp/index.html | 8 +- src/main/webapp/media/css/website.css | 188 ++++++++++++------ src/main/webapp/media/js/website.js | 26 +++ src/main/webapp/oauth/authorize.html | 45 +++-- src/main/webapp/oauth/thanks.html | 7 +- src/main/webapp/templates-hidden/_login.html | 2 +- src/main/webapp/templates-hidden/default.html | 22 +- 7 files changed, 202 insertions(+), 96 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 09cf87bcb..c2d3c7d59 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -30,7 +30,13 @@ Berlin 13359, Germany -->
-

+

Welcome to The Open Bank Project API. + + The Open Bank Project is an open source API and App store for banks that empowers financial + institutions to securely and rapidly enhance their digital offerings using an ecosystem of 3rd + party applications and services. + + Find more information about the project at openbankproject.com.

diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index b0708bdc4..be63565ae 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -1,11 +1,11 @@ @import url('reset.css'); -@media all and (max-width: 320px) { +@media all and (min-width : 300px) and (max-width : 999px) { #signupForm td input{ width: 150px; } } -@media all and (min-width: 321px) { +@media all and (min-width: 1000px) { #signupForm td input{ width: 200px; } @@ -161,16 +161,17 @@ input.submit { .account-in-content{ height: 100%; - padding: 2px 20px 0; + padding: 10px 20px 30px 10px; + text-align: center; } -#account .profile-info { +.account .profile-info { float: right; margin: 0 10px 0 0; font-size: 12px; } -#account .account-info { +.account .account-info { padding: 7px 15px 8px; border: 1px solid #dddddd; border-radius: 10px; @@ -179,90 +180,143 @@ input.submit { color: #666666; } -#account .logout { +.account .logout { font-size: 12px; } +.account form.login .field { + width: 164px; +} -.account-upper form.login{ +.account form.login .buttons { + margin-top: 30px; + height: 2em; +} + +.account form.login .field input { + clear: left; + width: 150px; + background: #ffffff; + border: 1px solid #999999; + border-radius: 3px; + padding: 2px; +} + +.account form.login .field .signup { + float: right; + margin: 1px 8px 0 0; + padding: 2px 0 0; + font-size: 11px; +} + +.account form.login .button-field { + height: 2em; + float: left; +} + +.account form.login .button .submit { + float: left; + background: #c1e7e1; + border: 1px solid #22a890; + border-radius: 3px; + padding: 2px 5px; + color: #58988e; + font-size: 12px; + cursor: pointer; + margin: 14px 0 0; +} + +.account form.login .field .forgot { + float: right; + margin: 1px 8px 0 0; + padding: 2px 0 0; + font-size: 11px; +} + +.account-in-content form.login .field input { + width: 100%; + background: #ffffff; + border: 1px solid #CCC; + border-radius: 3px; + padding: 2px; + height: 1.8em; +} + +.account-in-content form.login .button .signup { + border: 1px solid gray; + border-radius: 3px; + padding: 10px 20px; + color: gray; + font-size: 20px; + cursor: pointer; + text-decoration: none; + display: inline-block; + float: right; + margin: 0; +} + +.account-in-content form.login .button .submit { + background: #C1E7E1; + border: 1px solid #22a890; + border-radius: 3px; + padding: 10px 20px; + color: white; + font-size: 20px; + cursor: pointer; + text-decoration: none; + display: inline-block; + height: initial; + margin: 0; +} + +.account-in-content form.login .field .forgot { + float: right; + margin: 1px 8px 0 0; + padding: 2px 0 0; + font-size: 11px; +} + +.account-upper form.login { float: right; } -.account-in-content form.login{ - width: 555px; - margin:0 auto -} - - - .account-upper form.login .field, .account-upper form.login .button { float: left; } -.account-in-content form.login .field{ - margin-top: 10px; -} - -#account form.login .field { - width: 164px; -} - - .account-upper form.login .field label { font-size: 11px; } -.account-in-content form.login .field label { - font-size: 12px; -} - .account-upper form.login .field input { float: left; font-size: 12px; } +.account-in-content form.login{ + //width: 555px; + margin:0 auto +} + +.account-in-content form.login .field{ + margin-top: 15px; + width: 100%; +} + +.account-in-content form.login .field label { + font-size: 12px; +} + .account-in-content form.login .field input { font-size: 14px; } -#account form.login .field input { - clear: left; - width: 150px; - background: #ffffff; - border: 1px solid #999999; - border-radius: 3px; - padding: 2px; -} - -#account form.login .field .forgot { - float: right; - margin: 1px 8px 0 0; - padding: 2px 0 0; - font-size: 11px; -} - -#account form.login .field .signup { - float: right; - margin: 1px 8px 0 0; - padding: 2px 0 0; - font-size: 11px; -} - -#account form.login .button .submit { - float: left; - background: #c1e7e1; - border: 1px solid #22a890; - border-radius: 3px; - padding: 2px 5px; - color: #58988e; - font-size: 12px; - cursor: pointer; - margin: 14px 0 0; -} #content { padding: 20px 0 40px; + font-size: 16px; + line-height: 1.3em; } table { @@ -549,7 +603,14 @@ span.alias_indicator_private { line-height: 1.5 } #authorizeSection{ - margin-bottom: 80px; + background-color: #f0f0f0; + border-radius: 12px; + padding: 10px; + color: black; + width: 450px; + max-width: 80%; + margin-left: auto; + margin-right: auto; } .login-error { @@ -558,7 +619,8 @@ span.alias_indicator_private { } #authorizeSection form.login { - margin-top: 20px; + padding-top: 20px; + width: 400px; } span.edit_error_class { color: red; diff --git a/src/main/webapp/media/js/website.js b/src/main/webapp/media/js/website.js index 9ec5df13a..72ed29aab 100644 --- a/src/main/webapp/media/js/website.js +++ b/src/main/webapp/media/js/website.js @@ -1,3 +1,29 @@ $(document).ready(function() { + //fallback for html5 placeholder + if ( !("placeholder" in document.createElement("input")) ) { + $("input[placeholder], textarea[placeholder]").each(function() { + var val = $(this).attr("placeholder"); + if ( this.value == "" ) { + this.value = val; + } + $(this).focus(function() { + if ( this.value == val ) { + this.value = ""; + } + }).blur(function() { + if ( $.trim(this.value) == "" ) { + this.value = val; + } + }) + }); + // Clear default placeholder values on form submit + $('form').submit(function() { + $(this).find("input[placeholder], textarea[placeholder]").each(function() { + if ( this.value == $(this).attr("placeholder") ) { + this.value = ""; + } + }); + }); + } }); \ No newline at end of file diff --git a/src/main/webapp/oauth/authorize.html b/src/main/webapp/oauth/authorize.html index 90e15a8e7..a495d055c 100644 --- a/src/main/webapp/oauth/authorize.html +++ b/src/main/webapp/oauth/authorize.html @@ -28,7 +28,7 @@ Open Bank Project (http://www.openbankproject.com) - + Open Bank Project: %*% @@ -44,25 +44,36 @@ Open Bank Project (http://www.openbankproject.com)
- \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/_login.html b/src/main/webapp/templates-hidden/_login.html index 3477c6bb4..61ccbce3d 100644 --- a/src/main/webapp/templates-hidden/_login.html +++ b/src/main/webapp/templates-hidden/_login.html @@ -1,30 +1,10 @@ -
-
- - - - - - - - - - - - - - - - - - - -
login
e-mail address
password
- -
-
-
\ No newline at end of file +
+
+ +
+
diff --git a/src/main/webapp/templates-hidden/_login_form.html b/src/main/webapp/templates-hidden/_login_form.html new file mode 100644 index 000000000..60b9c1873 --- /dev/null +++ b/src/main/webapp/templates-hidden/_login_form.html @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index fee6148ff..d1531649e 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -1,6 +1,6 @@ - - - - Open Bank Project: %*% + + + + Open Bank Project: %*% - - - - - -
- + +
+ + The main content gets bound here +
+ +
+
- - \ No newline at end of file + + diff --git a/src/test/scala/RunWebApp.scala b/src/test/scala/RunWebApp.scala index 504c0dc28..a7cdd7d41 100755 --- a/src/test/scala/RunWebApp.scala +++ b/src/test/scala/RunWebApp.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/AppTest.scala b/src/test/scala/code/AppTest.scala index e0d85c72b..d908bd40d 100755 --- a/src/test/scala/code/AppTest.scala +++ b/src/test/scala/code/AppTest.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index b3c0edd05..13b4edafb 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/API12Test.scala b/src/test/scala/code/api/API12Test.scala index 91a7f72ab..46ae8cb90 100644 --- a/src/test/scala/code/api/API12Test.scala +++ b/src/test/scala/code/api/API12Test.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/OAuthClient.scala b/src/test/scala/code/api/OAuthClient.scala index 8b2aad0e2..a1edfce31 100644 --- a/src/test/scala/code/api/OAuthClient.scala +++ b/src/test/scala/code/api/OAuthClient.scala @@ -1,6 +1,6 @@ /** -Open Bank Project - Transparency / Social Finance Web Application -Copyright (C) 2011, 2015, TESOBE / Music Pictures Ltd +Open Bank Project - API +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/SendServerRequests.scala b/src/test/scala/code/api/SendServerRequests.scala index 24114189a..1ba01a28b 100644 --- a/src/test/scala/code/api/SendServerRequests.scala +++ b/src/test/scala/code/api/SendServerRequests.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2014, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/ServerSetup.scala b/src/test/scala/code/api/ServerSetup.scala index fa320f902..990e0fac1 100644 --- a/src/test/scala/code/api/ServerSetup.scala +++ b/src/test/scala/code/api/ServerSetup.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/oauthTest.scala b/src/test/scala/code/api/oauthTest.scala index 7a226b85d..42bdd05c1 100644 --- a/src/test/scala/code/api/oauthTest.scala +++ b/src/test/scala/code/api/oauthTest.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 diff --git a/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala index f651343db..32e6cd676 100644 --- a/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala @@ -261,7 +261,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers challenge.size should not equal(0) //3. TODO: answer challenge and check if transaction is being created - + } /* diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index c3db08c25..4fd795b2a 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd 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 From 4a13a9193a99c37cc0ba2190a4fea633208ef837 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 6 Oct 2015 01:02:02 +0200 Subject: [PATCH 227/702] ResourceDoc: Counterparty text tweak --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 1e39c6db9..9ae12eec2 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -694,7 +694,7 @@ trait APIMethods121 { "getCounterpartiesForBankAccount", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts", - "Get other accounts for one account.", + "Get counterparties of one account.", """Returns data about all the other bank accounts that have shared at least one transaction with the ACCOUNT_ID at BANK_ID. | |OAuth authentication is required if the view VIEW_ID is not public.""", @@ -721,8 +721,8 @@ trait APIMethods121 { "getCounterpartyByIdForBankAccount", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID", - "Get one other account by id.", - """Returns data about one other bank account (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. + "Get counterparty by id.", + """Returns data about one other counterparty (bank account) (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. | |OAuth authentication is required if the view is not public.""", emptyObjectJson, @@ -748,7 +748,7 @@ trait APIMethods121 { "getCounterpartyMetadata", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata", - "Get metadata of one other account.", + "Get metadata of one counterpary (other account).", """Returns only the metadata about one other bank account (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. | |Authentication via OAuth is required if the view is not public.""", From da57d663b26c6c27779f2daef940412a6a02b377 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Wed, 7 Oct 2015 15:04:17 +0200 Subject: [PATCH 228/702] make consumer key generation login mandatory, change some logo header looks --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- src/main/webapp/media/css/website.css | 13 +++++++++++-- src/main/webapp/media/images/logo_stacked.png | Bin 9394 -> 61587 bytes src/main/webapp/templates-hidden/default.html | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 5658211a4..5804795e1 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -232,7 +232,7 @@ class Boot extends Loggable{ Menu.i("Home") / "index", Menu.i("Consumer Admin") / "admin" / "consumers" >> Admin.loginFirst >> LocGroup("admin") submenus(Consumer.menus : _*), - Menu("Consumer Registration", "Get API Key") / "consumer-registration", + Menu("Consumer Registration", "Get API Key") / "consumer-registration" >> OBPUser.loginFirst, // Menu.i("Metrics") / "metrics", //TODO: allow this page once we can make the account number anonymous in the URL Menu.i("OAuth") / "oauth" / "authorize", //OAuth authorization page OAuthWorkedThanks.menu //OAuth thanks page that will do the redirect diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index d732d3b7f..e078698c4 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -112,7 +112,7 @@ input.submit { #nav ul { height: 40px; - + margin-left: 20px; } #nav ul li { @@ -150,7 +150,7 @@ input.submit { display: inline-block; padding: 8px 20px 10px; border-radius: 5px 5px 0 0; - color: darkgray; + color: black; text-decoration: none; font-size: 14px; } @@ -175,6 +175,11 @@ input.submit { margin: 20px 0 0 120px; float: left; display: inline-block; + /* height: 75px; */ +} + +#logo-left img { + max-height: 80px; } #logo-right { @@ -183,6 +188,10 @@ input.submit { display: inline-block; } +#logo-right img { + max-height: 80px; +} + #main { color: #333333; } diff --git a/src/main/webapp/media/images/logo_stacked.png b/src/main/webapp/media/images/logo_stacked.png index 81229e6aab5a5b04c14df1fdbdb9acf63e34d398..3ab75d05f4a9669880f60c02ddc749e6457e159d 100644 GIT binary patch literal 61587 zcmbTcWl&sA*ETu~?(XjHPH^|YU_*eQgS$I}JHa)$y9RenLXhAD_W;3z1pUZ;KhJy4 zkGH;4b*83j_U`Fr*OK15dqr!gE1;v0pa1{>bR|Vu5C8yV0RUjgkPu!+4j=OKUVn() z<-qP*PL}Rormhfxl!cQSgi6W5)CvNEm|FO_e1V7p0I=3J+F*CEnyQGolLLq8-#HxK z4$iOC0D!23x3j6a9mJi=3}R*DC`NPE)ki~RV{VxYu zh?}{qjkCLrlOxq%j;3Z#9`0f^ubTdAf`hY~+W#urHb8vAwIQ+Hi zpV@BiAjtot#{W9oP20yA!U=-7IeEC6zxuO%3so9bO*4f*>cpu zN&ks%>xTnRzhk6q2db{g60vB@m#;r&A5?SiQ-Gr_H5(pdM|raI?uho)BDk&elba9t zv~PECq@kdrW2{A&7JDZW}JL~sb&n}dE7Aj))f@PfI1D1<#70$y)u3)Li)$J3O> zmcxTd>j@n-)ram;**fSiZ=^@G_lJI|dh^Xg`$edF#ZVWHDuTVxxEW>~#Kvi=Z>C~J z6T?&#MH2&6mZkkj6e>YB{vDjqIG~iq;jN)02G=iD;RzTK?v#C6Oc-@#dk(1CX@Oyz zGWQ`E=LP2tHL;MOyb67ltPx#tIc%?}KIBT4UR=Q*4h2Gkkqq%>JsFVzhfrpidG2#* zHb6>g>D_Hg`;%b$SfhP(gz{gFHQ}^rSj4z@=4tJnT~X3$v)FSJ@aT|Cq9#+wuR?IX z_hKwjg;<5dgF}u9mS##+8tEEEgZHG^FroATN^&yHf3;@m5ivu5b2kj^^2_RRqsDRZhr~_3z+IB>;P)|7@soNnhtI0bl5w6y@n%?spqG34>K#_qVix8PHl zaSg$Lv?xNVkfkf|v?w^(zAKNBhweW~U9C~?_K40D zhB~`pQ9O{GZp1zW4PRQZiG5Fu`|Avn4!qdx0(*Cs^bM+FfAY3IGb;H0Z|ks<9CF;I zyV_SjehI_v&X90sx>l7A0@0VR8DO6APQKEC+k)o_`-WZp`9x{|@oGU6Cr)!tJ8&Gw z;^QrC$|K-Z@KBjzkjo4gi&i89DIvOFL4!PGH6IlQTvp=p!LtF)g>v8$E8R;@M&LiN zJ^&}A8UCpGjXay5FXc}IiQ1zB6o^(ZQCyK@*i^fbmcn4LM}Q_YIvMih>Z7iLU`^M0 zc-A7Pe?0pd7Rz=~9e6`FK}0l18)Z8;$7mMYH_SCx@ZYBs{*BSEc^DdX4Fg}i`Y(5bxsJ$n*l>-I zfpAcmTwhK2#2FZz_sBTky&0~%QFWl7xca|I0!OwN$f8%?^rLJA+3?N%7ZpZqm7`mI z3(M*O(3vWNjB`=b&NMtV&zG=o(>i)blCb@e(WuSTl1S*I7AB<{euOLLQN(L?jZ!wd z?1cZxGHkWQCDm9Z4F-f@_>{Chw)_Mb8GZ~0VAMlec69~n5Dh}ZvP6;cJB^aeyz z(dB``h`}0XrDU!IHAWf6H{_Ht7FnjCp`EG8iyB9=>?@fXUw`IpN2mO>$+7_ZsH^Gk ztbZz^oa9LAzKIXkxRo}x)({lOj4FIuQ~2jOnMX zLY3R3(c%{gv+1gl)6=J0k_Lh5(5=Xog3p1tH>?#fLd3;*WNYn2o?t1Dq`@aM9| zci1kg!bLHL7av@53X9j1MxVqNtItGep$Rp{eaj5IPiEEMHO>{{Vek|}F$3(GgVTe{ zs<{7K6)=o2b)D;6L~!7=dyMMCq#-ktvtvmvJ1Cy^Ej7}a=v-2c1G_U1X`d;j7-M1DymqYDS^!J&N3!Xr>kgnD9MH-0TvOc6&*x6;@hCHtGw zEgxX;3A*FM`|}IUl1-({aW$%K24%~xd8Bd9bG+;ZOO&EIp+M0vwHqtor7G-lR!9Ta z6NeoxY~}Q`0JwuGJN=(jxCt!aAsycVJ5m)8#>#k&P)1yY2Ghwf{_d{qa!=GtR__Ug z#(qr%!z;_;Ad>(_RZECOX41JzTp{Ho4vatArXW(=&lGBE|M=dIQzI*KPmXO2Yg*4P zlc$twloF0W6{t6$f>e+htW*5D9KDNHd7R+*y=h2 z$dTJo`^w& zdM$3<6u+#9CCrrwk=p|5^%d<;a?*1-%Kh~LnCWguePE7+vZ{Xv`<$%>)g zf~q}#45BXK{(UYYzTkr&R{>)PK>TIRN9vN(KTB6fUOQ{n4tu~(hsT>1iA<6DzPALt zQ`PxU!y#hJK(0~7nucudg!4P+>#1CE&^lte-b#h_{lAr#hsQ=7mU#}y3eUUO@Sa?x ziePPCkiT6X-XtbXQj=b88s^1pmf^ z#dlNidw|M`c?-^bBWscmVl>TC#DpZ7JWmlWOD~eJm2|F(q(hf9-^MH|f;$Lp8tbg6 zm;sg4Wl^>hQlvgx+mH)of_Y|70YB}mNv>bj!G2G(SSdAR3&yUoF6WDaIi@WnRhk`nyJW9vE>(j?a9p714Z7P zh6ji&X8USp$&!?1o6E}B3gC~AgKuKXTK)~zJ$eQuACwxMV!NiY?3n~;~pg~3T|K-P(c*!PfTks!t z)PCCmGuk8O+4Da+&a%l=o+~j$Ec~LK5)oj?8uW@%&Ep2D&S`_ zkM^jc_j*WX&@rfnqU(+-Xc_!#>CB8Q+uJQTLkLNQY8pU&gid>1stiQ>JwT^XK$XaS ziHL=PFAqfd9q?yMqY6=#UUgBI)?tx z#E0Wjv@Nf`xj@(&@=IV$|LXpMzkxn+Kt~%V#=P}!i9PwkhvW7n3IkJQm*qY%9Imaf zR#RxT3tFXQ97 z;ar(1ToB#1{V~}WuSEV$FnNx7{4bKo+kIJ}bF>zZ|39Uc(D`o0P zpo8^e=VYY%Row#;FKX@a^Tj5ipG|y`5I{0qQS@rFWI}WFwuPmg*gxzH3h~=&+a2m` zp$pjz6}457*|O-`NkZDofKYsdcbAfpt_vw&OaAaiEW3eV8|<6~-5DPWg4!`VBRmHj zSI@|gg2~$@GD-FeBG#G-@~4h8Voey3`r%-tjjnYyhPSxtmYTQ;7+mYBg5b}5#M3EF zI((D<=$Y~33{xL+@9XDMI5;roO2ukJF8=I7|g4BVG@myDjpTi?eN+w%X zX}yW0HD+>tMLzEWfB@jcJUUlv9rO`H=ED*5#ZS7$4Fww-~H2@`xN zcK2^nF;9%sFo&nyh7KoE`;!eAgZs9jvh~`APfm!xh?bB8m~|J0<`|C(f`jScI9=d4 z=dNzFwLtRHzpaRzi_K9>zT%OC%sw}<-J1r( zT-MV7MyVAp5az6o`!vdufw6`nOboV3g0rc}-b`}cz#3O7N!Slr^teh=cwU96aN*in zQ}q?J%mp4|2L6P|(WB8OOhDPQO`Xj)^;Vi0m0tIO?;n3b&=fF7eX zTB-YXdho#*jge%5I8wio=_T?Wfjqc-4)2#+^pjxMf={m2AWGPhPO0m6+AmW^9mlei ziZ__HVxcoM{LrbB6d~`pu66&w3Gu}%N8L9jej3Mu$4_$K23@!`_*82I@K)$xMiJ(#HXRBg=>$CF4x6d?Z}aXfyI^s-I~0nmg9Uxv(t?Vs-MZh zUb-0M8E-RtJ~@%r90kI@U+zUq_o|gTZ=w*mb=>phQ`3o`)?FFY0lk-5=K`T1Q3tT% zL5~#8qeh9E+uBTh&|kbQhgGzGa?vUAqK%1QRiT~6-xXn~@PkJunb}m``M5%k-&Dm_ zB%%5@qM0AA#ol+1o?h9KhRXJK61+E#`zE}195M?Ffg3F@{RfuDQF8_8lJ zWUVOXx-ZQK)j@kCn+~^u70GEI6Cm+%7m+ZOdv`Ia#Jb>H<9Xhw#0$_8{~W4O=$b}; zML02gis{Quhx6u7A7_)XK(0oc!S`LAs|#)9=_iKBDxPeSBe~;$o;l!buXQ}Qs><+f zw{?Cz70fO$JQ*TEbt^H@PuD&$sxxdcc3*x5{lqF!6sxK4iGocRA}8af?6#X%ryC;= zk@lkz2Z9ZmLc#}SS6g>lf4pZZ6V{+^fdAky@H7uj-$KtHSdoa{aAR2cqW`7hlZT?P zGOeb5)a?VE<6DzsxNfJ_z-~z#fTIocG;$Nneeh!{nXSRglEZlA;%we~SI+-D#rv13 z=t481qBcgq8@P`WCDo*Fw$VEf5wQa9;Uf^)@DQ|d#aX5Pf#7O*LGMP})(7uuThbZAM!Y;}ZIni~*IEj6u|u^ud`) z{08!sDB^>J+B2vv9Q!vrrHsSKJs8u|auQzn%iSxSvF{$d;_dPB%WR^ljfbLEu;@$Nbg`KG~uv$Dht=>>gzz63i$q z!I+4i3<7R8f-AY0MfnDyUv1ijF+b#o;b)~BV@X_1R6Dvub3bMoCOil-L%#-s>-9>_ z*)}L)+nWBZV{G&Q;MV42TmwQ;By zOdwvK@!O2$9Il;NZjTe^r6t(SX>P>9;W8Iz#W7$7bJF+YJY}a}?-1)x4dFTM1K^)~ zZm@rlbC@4RiW92^aFf0x5)9;D`86Q&WS%zrF0Wcy~HwWP-kCQGf73L zY>6k80J=SCBp;LUtlsRc5BSuQxAp%qAn^Zk%1N0*jWHNXxaq{W6^i+3 z9rx0On9P&0L#)d24Lf#~#~QN39IRoSN)G!znez{yAa_+q4d@S7 z=K1&ixDwg*S6yFx(qLEwK`NZ3aEK$>yK1sWQdc_EGa1sQg5P)eaOInq9{0jnFw$W3*9`*IbHvJ^-LN0C_62;>vtR8+>HD%BR(Q;4evN|z!3 zL_%cZ{`nT&HN$9B1C!DX@R4T^4CcEATUXddB}P({PjlyIcuo57u6}x~ci#A+Ak1n( z@s_xbqI~qJl=N2C>rr#$$zyE>HZMRB{M~Lbq2<~1B%gs_xH$@21+O<8|Ixp$u(H^r zCncmlh50TE>AKdv12MjjcWPK-a(G9gUj_->?otwy^RxTA(rO@sptB7?dXg7{0t1yU zgDqP3V%!G2T^G2lj?3iR(OG?;X&zWwT2IZ}NAM8d&LSgu6i<7C`hi9M^LY;i$njig zL6=jU=5CYlDWP;J-E3nA1zW?)R+e4ZL`8e7$@Zuvyj!k&G`@l9T}9_h+KG=dM#-Di(4H|vReiV+rdW{L zljDT=N(z*ChTbMw$=sESDdBB5ZoUvYrhm0;zUMh3aE#Z2t$F4*+iUmP<8Z#$!v`^l?_EKf~s8(uX*y0VF{+Vk6ywZfFSQqijl#aqD z56(leZT)AutEjot=vwXWBe}(jh)5Ov^3zL9!}G!x_W*{$XZm!^_!BE5;pYmE93ZIBV;zme_kfq{6`p5_z^S2h_d46y&>C%hra(Xn|$1hb>6<*Hc zz?=3GMyRy&rG|8MOXwE)Cmpj~x0auIVcl=USa_5S!m*z(qcXMOTPCn7Sg{c~@ z^5jr-D$FEii?75AdpK+dc-hqoLZh%mQk%DQrevmQnSx#Uxo)&Hiy3`4Bh5E2#dBkAgkJD_s_yLOuv|L33vo9Mi;{t z)GMRe`Gp@Cp`%z$#Qs_?vhelSFS_}vX3 z7}|efeNw+LtuAN%VJE3cQ!s=Bwb6v5k0wT^;M)~YvUX@um@$rNBTTANcaBF19Ti3* zks`#SYXj=zz`GgJaWKe@ixJ*YY$*pQ`ZjE@=P0o==6qlm6tThf1Z)3mZO}n4IV%;% zIwB1ig1Aet-AHiREfKoc)Lk-jLMh{%93eu)P~5m)nZ(P5^oVY?Aoxb^_s7bpRmX6& z<#g>ts`; zlzsj+PJ#4G)vpG}W~1|g9x)w|FxdNmA%0G>7M|Cb(=^pjBxzysEm)bjrGMVDNlLgt z7y2aaepl6~^{v2_MJeg_HZz_@U&uRM?vA{9Dufl|4^_)ti)>Wk;7r=>i5CT#*fUo9 z_E!C1)#)d$W)GIP6&1}UR<^A73{oYSH6(p2iHGKN zR6d{?l?`x*E)8*!v=7Bmrc>de(ckwSxdFCWTOqDU1OaM(7MEj3-^&QU&#fCS(&8PZ=mKl zd=rP_S8O;#6#fwOcO73D0+xF|%DEeQfv8L>eOfWyT^PLQmSDFIVtbJnDLf!>AbG3_ z>s$=M{7VNSgGB^8&)QH~wmNhc_p#u^fT;a;RFdOlyU{Hl%JE5Je0EQEg|IQqaLS#& zGf+Jmd9}HQD$@EA-Lm;ybJ6cAq3)DoL)Ee5u61Ieh>9AGv7Sm&y3ccB>z)M^{V6-4 z!w&V*m87X1Pzr2VrsSu!KiF85xC*19u($OKW*E;K;+qJ}n?U>Fz4yNoi(tOArL^^R z2b?_}!N<`f@{O!(v$5WrgBbSgvATLH^tKP+);qikDa>tr5l^S)?wyf;5+WLEs%Mt` zmfU0VAMFrf?V>VZU%oDi&W8GY_AUBiZ2^c337n*4kTX@$O)oC1?Zoa4{H@~*81W55+zcN?V=rZR>54HP!bx1PzD2FphojSo4gaQ)Uytj@Gr$Eop1 zc7)zK5sA@iJ1R<9G$LmbzXvhXJ2(dyNfVvEiWZqhh%4m4(9+Pb{N_QMNUvMJGpPpF6xt?tHiE&AKSHN{=uUrZ*hl3n!S`}f_PeTZDX77S z*Ws|fFC7o!CO1tlY@ZgVKDg_}nTk4v3~#YS7u$m{tJPh&RbWV){|egV1p z&v^;6vr=mmCEr&DAn@4bKy%gVC}r=?i?G4p0M7MAt6Y7I{*$ZHnm3LN87}F?BlhDd7t1?ksiQ65+FqEx_w!A`+3#o3iYV-< z*Q~TMG#XrjQiae;5KT``7ftPr-uORNRBw{IZIDny#iB^6NVoLL0|+@f&$Jqfjy*DG z1z-o1t2Sj*OzUYQ%=P%?$$#s9ESVZTkzj$>A*wH0781b!{(P=`pTTz;IbA#brNYfH zbs2Lt;kE429cQl2K|XSc50W9H;7ZEqn)C(k8^0-xv)}M z!VZEZfJyI9>o=@t)`n!YB~$MQ^`X?ibiQv(G2ZdOWt-l~;MiL$hdDSBoh6_vt8a*2^Y7JF8qkf~$GU_8mi;w2_-N73*oj=_vbEqi%^Wz}OKoN|r9D={nb!Rm-UZ#OU@fCWW7NvqgJ z;~xF-L4(acq_3Qfik`?@7#h*<8HF_ZmJ85LJRV&Wusech_-9r=3I{KJSUw!Zj3?3g z_QIdgw}4hv@q$Xe$jvxW5tL~(l=(xq{dt&kNGCFoXjCvER_K{Rb%KAw`Ksu+ptugd z_1eodr?>%?0{`_?v9KWr!AyfP+f)Rzbi$5vP@LBS%a|ig)x<47eds?A z2ECM|qKjC5+T1azbjid%jQ~cjh;X16q{5^JMZd{J;UU1~lhJy1#g@+2b{(Xt5kZ+ZF&p5Z~h3& zjIy{*$(iaQT@*v9^q!=|LrEi<`?$@o$&_p4#fHa^#@g6sll!a84?>S3se7h$9}y5L zsK9!480ovejy|*}hMmT6<`2p3&yJlq$|39>%=q@l_tKwqJkBe_GY0p2*+qS8m48?s zWfjAksgAlS-Ubpp{OrDLb0lLd4K%bsmyUp+B_s+>Z#T?^e1unhD!e54Eb>iAxA^AN>nTo}O3n2W!s3EU<@2X?T(&6{LGF)1{=m-F zaShxh+65Aurflf zT%L<0Vep&XIu6b9Rzw7HgzWrW!)tO~=K*?KP_SEXx=*dsG(olbmak7~s?ng}7qS=6 zDqe6JiiMhGRb0v=4s0ZK(JTaytZqcI50R}q(B_b&rk}6`Z8_XoN!`di(&U9J-Sbl{ z^9ASE09^jGDkMebh}fJl`ys21&HU++({DGL>FB=QEmQht3c>OA1uE^=x8~r4!5M>S zVJ0xV%wsfDqt8!_X+n4;&~({Tzs)fR;(loGucogtUx18wO5e(ax#--iz>g| zLgyxxB2b!-A+RV>BwF^YNu*JT)BsV$)bGXf?%N>d!0lU;tbTK>@9Rp&Lfg1pb&hx+ zB5_NAGsNa1V`@4QBJbT$pmV^ONA8gWC2kB|q>@^v^6pk<%6Tl-D+kT{BgZ=14?q?CD+xc*wOB)vL2t5v1@kOjX2UP|ks znA$+z&kotfbMRs_b^@^%*Q)0|=GM>XNCiWG%EFX#3dDm65%Pu8_0RF;$Q2It6{B7_ zB@8;t7L_Gz`}Sdh?BGm;Z!edI5&KQ>8Kq=SW3kSDDni8t_ab30ZoZW+(bYS(zFeFI zvxeVfoaTR^gyo(2+fmvV4omzLRkHrr%EU$(-?wyB)FAS<>$Z7dKI{lqy0uH&%@gc< z#Lv;{m8i71PlxVf_T8?$^8()mre!sLl|tTW=#9hv+nrlDCcA!0qg^#I!j7K8N8+q# z!t&^%pB>N@ywxW^EQ;@Dmpsv#J4Ue()p4LKJJQO&R6t?1Co-y~s%8zoRM|H=s z2YHXgxL8HJ-w9~axQRuoYQ~y6n;h5(BKOwnhp^2t!;TXkq=y@CHEpYGBD^MqNndvq zwpjY5O9IMbS@6N!@KQ}yP<)v}wsASj{Nw^ye_xiIACZI1rww=OV3PE@k3!N#JI{nA zW@nev84YNVk)L>Q(QDnlq$p!%=*LDkBt@cBbP}+grC9HKM{qOt^b=Ro6T& z8C^{ZJRt$eIKL;ds9JDc+OL5wKiIFb}E4v^0kSKoRI-KF3}~`H^J~?bR<#Nj2SW$?=f(m&9u7O2sA_C}@kQXM)Jx_v#Hi(LO$H*z=Ty zC!HxTRnD#ka?D%vdp7-Nt}`)QBv7W>U`Yh`d92(MP)#FKzA7DS2jwq3ius;mV%Z)EBUhwU%EjuE~Ug?5ZI&v2)K zMBUTs&*XMz)?GvVqDrz&M_~I+iwlv6 z;oxvC6oJQG2qh?}D)l6DOKRhuN>+pDmn!A)!6D0yD)RP!GFvspGn_^i3cE7#_ptFg9@83YaM$g%@p>|9 zMF?&Dk7wUM)pB*#k&{%D-d40`uZWZG$7iEgd)0;u_#=`}vponz!z?it9F!T;^}3+0 zTLsz#EAwkxc}nAlr14dLnvJev4UW=h4qRaa}Jw5|7NSFoA zI$Zr+sdW{=lgX%&{c{=O7Jk$N7^pfO?Vs5ck5ef&j@l#^Z^@ z-AaByGQfDl*Hv17!UgrT4`UN>nfEa?RXKwEf`Kv2aBxr>io#ovD+^=*kt0XgzBX%q zN>$fh`Q?X3O?ygu8k(Q`h%QuMF8<4b4+`ChUnAYTUOP4&I#p}v_2R8LUWQwf=`vk( z=A`3Qf}6&+BRjBwiVj zok0xu(n<;L&~oHl6V#t@d!pkp;v&n5xSG?b>~d&IVHS>8V0NkVbwk@@e%J4hyEM2( z^f>GNXOAO8%L)zazy;M_^{X386(XNuT_>PPvD|dW%7%u3p#0dfJjqHRvG1(f#R~a3 zcC)9fy&oY-5jn~LB;EA>?eK}5j2x@uM&vO76oQ!-YQ{RS#e5fM(D;1DjB<&uBxSF0 z?9xPYBlHJ#oICvb4-bBbp2A9d2`XKV@3yI^0P!zFJq~1@g5OY-OkQ@yqQNS6cJxH? z=_3FB@It2|=H~_dtR}9*anHI4)LTDNGJ!f#f$-9@tvM{r>zA)LkOuEDyM{V1T>HZn zn68u_G7asq>H=y_O#as<3CAXA)Y^kOIWzY!nV>gXhy^fkvdl}-o7P${p5C6vW*CW? zaF&+i+Cl{Z9Aw33;|5w@M=zNU^gS^3w>?0>!(%q%6ab6g6fmXQ?NgsQN;m18wN`u2Mz&QJCAJyL`y z9HER4yleRG9!>nL>vX?J{K2X%!%n^F;?01GBWglB6VR;HBWkEwSA{J*#Z^-{>NBvw zf2f!AA{(bBihS*rTZ==R0%bmhanwBW6 z$T6m7zNDm=q@`lJTaGF!t3TzTo8IJRX1F&Ck4rk*ABo|8MuD#WdiWkp*vcN9(bgB! zZ%6+cu4rOI0UEg#@?6vX^X#>s*x|W(u6*K@%r9waN42F~bn|`lqC?-*%@Ng8RtjM44fM9<@ZjG{WZQny%#>lto&gA$AQL-jS-2&ic#j zFl;Y*QmEaNLgQdkCCvYY^O#O`7F*$Xb!Zqpg+7 zT>MCH9epvL+b;CIUB_b^N}b;5eQ#M1aMKw4CDmP>Le)?7+mGlli|s7KjmFBPF}^zV z-JQjNK#c3wFSDHr+>3%0w8M@EuaT@#y zTdID(0||WXz#U>+xiS=u9&z{#CyKC=Ttd#!L)_)4O5P^nh>4|+hWESVP3i?p`GnG` zw_z$&^SMw_C!usXuC2BE#L%^UfQ4HU?*VoXGlsz-C$WtGX|C>F>MS$`07&sM+ezt zz7GPK#Ggztl!ESs4jc;bEs<#Bp%3_z9lPE^T%}XwA-h&TvruVvPszL5Nd1YN_Zddm zpJ=IwLIJVF_knwr$6L=H9|q?m1K}*&CMvwUrfhRDFl&Q&?CZFGD!_vlw+vs!-TL3L+=8c>cLN5Wp?oUROd>1=smj1 z+G!k~VUm$t?&)=l3zx)MzyJd0>MVROkd2f({M|wKV9>4az%CKqZX82+3O6>J71I14 zriGBbOV)cXo+Q-YdbbaE!7AFnFJvklvxwz>^JoN z?B1IeqBp&(?Nr(v^0zAnPY5p`sdF=B8)M0o@NB@O_-2RYGO_ijS=OOE^wnYIA|P^3 zbCc0r)qAdH9~A%n^AD%kn{+8kLqQ7?Fqrj0WE2~}$=uPeCH;&@RUNZmpI(uGK3j8w z;k=q!_>?K`pXOU74b)IYyF8Hn?}F_%x1ToP&+>x;Yrcl+9Jtv7&ChIXQ@mfq{&lZE<9H8R^L(mu=om64? zyWf8*Xc_&!5qpWqyvVHMeol^P82y!mf#*an=UAI?Y(GMlqWVO6n)kZ7lE~kHS52XK zE}M@i1T#7+Z8G!$Q4LOv!zO~QFg-vW1EAc)NtWcyoEGG)t`k|T##XaGp(RVHpCZ6$HV4_;KvSV=y z*zHchZ6Sa_0sl2{`ofvZ#Q_6o_^rb2_S~_3Yq zg3R;c&0i2Ff5L*mO>4n!Yz^xEwV@V!X?F*(DnON}SO(BcM4)f>1!3{EVZ|xe1%-^y z@&y}RBX8xB=5af%YxA&{Q%Xw>m*;DxkE(4bUK*WJ+uD$_it1@;az)hsxHiK?yb~sw z7-nGKr7>5r2aEq*|E*q$#3+>zna(WAN@Qn%m}t_PsP8hg$Cg!^oP{;$R|@-OjxaiD zt_cm30MhH}fkqjNEBUuIh3!SQu1G?m_Ws1#>6H9C!N&vN4ZjYXl*JXgYRkk5F442I zN-URqW~~c1*bon&CXtQMJq5WGz1TVHFp+a?m+Grs*b_WauV~iQ9)+r8-gOU@s~Sp< zq1?!GU&}>;^4>-|FrE;aw>d&|7r1Qi!>k4+=*}*xh;@*TQM<#ZX(r>>!G$P?#Lq!& zbWok&e!26qjB!|oh;eAvYr7#?N&%ew`MHWAT6;hYqIUPZ0KdVPSv zqwg272D5{znBPi9I^=DIrY>In4dmMW@ANM1e%zQ&HXi%vysot}wguUz(ClxP6nC{@ zEnnHQ2^C>hLzBauKV&gR&Dz<|8p?T}$ptb;MqYH$qf{Gx>lYLjVRH#xWl-WUzhWL7 zkb;$Ob-GaV7xwFJV9y}WRy-XKDvKS6$vo{^(9+$5!r@7m@E~A(0+AOBxS?3yk2z`oT7W+I#et3u#;93D4D|@!=BlhLh6=QK7EB z=k)x>O*PI&|2#{6E&A=)sFVUklKSJ8{dBXouMdNo{zfx0$D2(tt~%3SQLlSEUbUf4 z;%=~_y@&9b>mu+++x(&=_7MoYV*1`lIxDkSi^sp47t^}WmuT@wa6AmtgVvfQLJibk zOy%y&J6hqB4=px@5JH_~A%vFq=%4pha$9d#Bg3u4@yB6DGPty*JS-9CtQMr-`UfdG zJN-yzf~Nm`OXQz6_IOK3+~aoGl86YSWuRmp1pS<2)@V3Gg=K_QzA#AseqDDWE(4R# zjD0;C5zm{!g!j^WC?hdjp_j(GLg# z-=t-9^m>RlA?e#W4W-YD$VNsuLy~%rxZEjN)hYgIe?%zEE_4{)%qvY=Prir#Y~n8Z zeS3GtpI|k z=pYDLtP&=3Lp;atiYrl?Z)G&BQghyc1@?ewS}iAUQ|=E@?7SbBIHD~# zny;-!cTKn0DWj#lHuTe=V7Mwof)+qxG>XQxnD6M=JUtAH_bnPMYs;+b+kMRsD$;K? zS-8XSK9!8LOGLbgOO`fTM(4mpnzH#i{N{d%lv)))eijiY74kz}m9f!KCqOZ}6u_lN zq3gvD3N%5UW0>?xjxatr>Y7ES&&_@x$9mp!@?5h#sqiqT!Fmxnb)R3K<=LVL2(wN= zFVDdD2p_-wfe7#aBFohK3Q@YBM7(`ZlPz6&dB)J`K`xC_W_5gpAib18AT%4yv=IIt zIlT^?tq9Zb4ZPxC`ZG-^+Zzz9l8qA@-(#?;ZQ=NO1Vf8b@QdtcNWH`#+Ph0!38;4s zie;ROWLOe~EH??z3rDI2!*maxJQ73LfB=lKj0 zMc#@5i|Q`WJdEDtQ=uwOHjc+?lqVf_Z1iPbP+*(_BsT&O-mzCE%If)A?aR@l;gFyJ zB8_R@$$3bxsbp2E&yFIcfgs8I<;FNan6w;lvK(V^QT7?-90x=H5FbNQ9Uce3Kd=?= zBooH6ya_j(SA|OEsMcr7$w?y&=!L4+GsoTY(2x2ANfcJz5(;0s4|u~CS-$zCZ+!=s zq*^q$r=CG%eHp+ObJ`F|8dAzQHVfuW5O% z3tm0jpYIz9Oz==s%WLDB|8EInXx-NPx+7CizACheG0LUVP|Rc7^JiylRS7@}htSsB z>APV64;v~mG!+rO)Sju#T-@uWibJ4F72u^42b&nI*KvwFbYU|4P(Y_LfmWhOJ6E~^ zg==8?KLDOUVZXJBoV232|G{0}*ctA;F@lz&a18!4Q`Yze2mW_3&D?(^xA$iJeNn!; zR1o{bKN4_?`<+XG;x)pQuI7k*Wog}vssEmEwI6j$B!uQL-ryY~Q7NV1vF>Cm52iw3 z@Bdr@E&P(0NrvwFr%qLzrv!lUpWuM}i(4{B#Nr>9 zl#>qu>QX69HNHur5Qd%t<5DY9?E)Hs$Jf z2&=MFeCy5l_gf6S2(?Rw0hP>JXkh9e(o!nEz?jxbH{bNEWW6g?t5N^yTFDwMnMS@P4a&s{oPo#$}{KZo=Ie&NeT0Hma=TqOjGuL_|fv zuX-U6phu$e?~}mW?D#5j*KC2^5;%;NJ!9KrlVkzjgvR?`9ONCKGE6OII&`9#gaORG zMx4R2xAt#`!^a*32ai2qM1Yj_yEnka4}2PWlY`tiG?hsyfr5^AO_;VXq)%x87&Z0W z3IZvju~$VBJUVZ=i5c|DY~G6b{)K=_-FQO=IlL~x z`VrRUM(=wN;tQ1O1^^WPi~F;1J1iA5`;rrDEN)B$G^+O_0ylG?WPoZ(0TlLDOtST= zvz>B%Ok5(yd`nOds_=S4<#GU72UXdV$KvTn2YooYE4gCi1u|i>I?r=^(Gln#+68A% zKWRjOlpXy$;L-;^iy&oyOHz`Vl#+ST0^3dn0>gq!m6T7{ot+x=d45qwq-|G9N~VUD zG9*)1WfQL-oxalX7Sdgi*mO1?XIXg%ip{E%qy-vME1@Q_neWGW6!@akE%>50dm^sN zr4;#~YVW}EbFhNnm72bZ6*$AmV0>m3gm|PFIgg6#RfCWQeNb*jEs!+j{&WIWD{l8*!k$e8Jk#%p(!1%$-jH(|t?%8y(}{2jiF zmf`Q$ufR#;p zW+~p^j#qOlmM`Q72eFd@5-Zdpap(1MaBTMLSMm2%Xr~mFNMc-(m(`O-;I{+h(>%@$5rZQ_-6~U(jlxWX z(kkCLj49llKB6;G>9|MI5I{~BWKzkBQ&vx}N{kuNZtL3tZ=SvgY7?~;{c5@fyWrCM zKLhLgw!!q4$y$;W6L6rztW=;cEOS!<4r>0%Nf&;(S%{`!RBcPy`>}hdm)pmHCcx8) zqoNy~wj}YQKwBhzL1|i)@;E9@>18-+4MB#+O5&=F|9li2QdS>YC*C@_{(L)@*PIKy zBGCm{H5T{dB3FVzr?IKf<|J3Sb@*;y^S!IwU&+4;c6yUIy?|VolaYm2WMIq(&d5zz z?^j(++}Qvw!%mk675iraZl1*!y|fZgMfpuqLDX5VXN_j~2F16Nwv5Zk4%f|zSUSnC zaRPmxeF@0}941i-Fk)j74<`k!oK>+O3T13TOw2USL{Ltmp>z^P*R{f;DaTg)+xqUa zH^J+@n_wn_l(Y>}iV8%j_5i+-=U*I`+ zV3}W71JeV+iuG{^mIiN!W<7wUtFgSw;d0@apu|_i_i^%mhh)Xq{v%9Qq8S&&X4LO> zy|^zwl0R~)#|!(H`0s-tiHuwN+1gWdBA$9YoOGm)3d&%+EwTrfSU*igAc_dw(d{V6 zwyjDm09T){lY504GA0vMP*5xsRX^xXuH>PRT$O#AzS2C7;5-svw&|q}T3d=7GZS9o}Iqg#N zd8};oFQ1ssRWW#7^?|ACppeYFLH#9;A34bpzDLYw`S8pu4WB&Z@B{JAW`e3>|gtFmKeXvi4=?U>EEf+yzzfs?a%p zsNr~~27RuQiiSy>;JJp)9j}z4ZBwKbn$t_5GqVl`ViTY`)5%prC79gLM$Wy?*{c53 z^pXMU?i?2Hh3bqJS+T! z8ITP*(6Eenyraq~Tks?cGy^wRAagtmcV_034dsCu@n#wJ8QkW={GW(_vBHAjvD`o} z+kL(X%dn&0O?o3{flgTN|B(p1(>yqYIHC&kS93=(cD+wf`x2sa-cRv?C*pZ;&x2o2 zOc$qj5O`8e2*#Ss>!%%m+uw90XOdqh48SNvBrUtgATrx=4Wr@MiHBDdsQlo$n_*M` zcGwS@s#h`*7PzUHBLf@^5HYDp4+0F9pJryuRG2?%Hq2?80drcXaVb*^0u~~%w_r4k zfM7R*lr4SRVa@J!ux|H8cw_f^EE~Dhr9U+U2?RJz2wa-+UU3g_QGpd22(V-zjlj;q zZa8-QLTIfm@4|*j=yqXywjsz9^RB`?c1*{9slPA5`hC)bimH&Ut#0MF;ZiX!OtN+TUo zFDe>u0*PfpG7-%;nb35zAnQ)jI*Hg+&sH(A?4aI0&1X#80Kg~Ga?&3Hlvj+t;MENGtthjc7}gRvyyRYvu%j#tCz`q8}X z-?3lrc-KG|Ebm$aOFLJ<=KgK4y6X*C-u*fZAaH4`YlXJzW-FkVCkutnI8%)S_AL!E zN!i5mOUl;uY=Ay|_Gr?sWb~$q1%;|!9jPy<6NBm`f7N`Sg_LFV1z21B*3G~$wrgwh z8R(7Ahwj8-P?OmT`q@B?=x!$6Kt}QXJP66Rk}GqrN5VjZMBUziYFT$x-HFUTnFfdR z{uZ8pqeOBLBv#0a+WT?;2&~1|KMVK%nUe~UEC2$Bq=@>HpLs)r zIKYfEg1r!71{wus-OafugxM|K23G8n54ZuEN2uilIj#)Hf|LP9$-gu^l)ynZx z8XLk=gJZgYf=U1^EwB!t@!eA^#QFD1(Y}<#@jI!7F+=}={`f3NlO~+VdTr%Q@r9lE zR=e@3zwmlYP$v_2<<+6VNX~VNXGRsNkpvx)l^pn5-8%Gn++HUiYcxLGVNJ9t5^teoGMn8 zvI#cG7DF^y>rBoAsX>_1H~|i+U?kdKF1`zTaP>X0X#%$kAqMScB57iFBFxo!W%uha zzilR5y#L3MiJw^3cTL2r;mGla@-mnlfcw`z3iq#j6kgi)D)eDrjBV(E#;SS_)=Z>k zeu+^Gwjcm}|Gaad9`9FfdSmwnXsA-#PO9SmgaPEmW*lMERQ5TilnSV@C!}I6P?OmN z?ZXek=9+hMBiqt(Kgv8h4!{QZgtXI$n>h)ANkB{ipN`&X#7U@cC7zd7WC2d@p`XWc zU5HhTo4HYNb6BU8z!~v$ZzUG^&wB5l3p?B!T#w58n-Yf|iA!aqP;#qP4&MPQVBLTZ zZFu@7XJ3_5`%v|`3;}cnbPE<1X8+C7Y~-SPcmTi1Mx_IPK18*GD@Lex&|t{L(C5qQ z-jB?3M*AFStF4T2_Bv!}|9s#c?@6|K38#eT8MxfGA5NIe?lwn!~sibV2DV}#6>t-y2r2;F-*l1`^J_5ao1<)Jc zkE?)aX;eUrSWvLt_<%3wSEWIdBynXM4nnFq!`Oh#!oo&&tw1>wyaso^TL@Bh8H~z+ zmFWp5gV0ey z+mt`7_&Wy>bqE?n&IHc+P&RIrQjAy}I)`@g49r&}K>76HAFVh@@e-40eeQ@)z%8eK zA3k=_h1@vuwXRiM%EAn9iGehU{mopdj~>4eX1C2O`@5{@dL6d(Z-?4=O`rrtSJI+$ zj)^O?>2(G0mpZ)vF{O&B!q{mB*f@?s$MF4JqLP7{Qg`o{@WJngoYw})N8FTI!FqD9 zi5i}IL#zjZ6nW=ej^}^3P@$)L;vuT%$F+US0ZNvyvDDKvw(CNy_i7(7QG85!gI4xQXV5pC^hVxgAZ0`E zR_Gh*g&!UFCAe_GJ4f`4Fb><|!v|gfzdrSQaM6C}K_-*p#<<->Jys@L&*%`1l^BUb z*FZNMIsTA}yu+(ISK0uJD8C63a_kp+9yPot4f$>3c_)-2o=X0k$iQ%{9U9Y1p((ut zhGJuxlYNPMnEV!)vjK%CEQ3}LgCUY$rTz%*fgK?WhC_(gwt>06vJmb!72)MlZfS}) z`z^`aX=IK3OoHi+xQKAt^djqX1Ok6_P0C)Q=qw=hW`b6J@I>%|5tq=#XHSxsjHD%j zh2zeM!LH_a82CCcaur`88lVru?{zU=bUFB&D>#XnbwWHQ_)bnX8BQXxa3L!+nN5%B zq)rsfJV{f~R?`fJjXS6!BmX}LQr7iu;V!{4KqB)V(*cIeFn4{=W@t*(!i^_?0~S@l zs5C4wd!4Hf`zQjI@4|)qy$#xHT43$&4bYqHM*tA_0dA55DFn6+bi#u6*>Gl=?QfhC zm6~|9wk?M$(l61t*2I3Nz+67s-?n}2qH?r3Gp21zJ^|IV6H=tH9Lx6Q_=0c3@;Q9d zovydG1EP@}Q>XrwagG-ePreoE5WNtASk)!HS&PTMh6fMEtva)?9`kuZ-0{Q_!PT+M zCFFUtplXE~xmK81{1fJK`&lfj{LyJ0uE$gW?TH6yD!HC4A*TC!#ESp3;uCv7Gr&5x z%#MIJl`|#zF^E;}#J{>t1r&ybf$3YNLPO$3FnIv&1u9HdK2O&GIN0@9Pt0r*K3xS? zRFl5**e+b9&uyClGg_xrWYjP1T+TzRQ%a>WH>xp&Mc2g9tXAHvlOFXisW%ovzOoQ*kzf!X$8k|~xe=^DJIvAU@ub>7my4eAqh zTGC-1&p=(nYFWhSAkbfoY~_k;--M;Tv@OmuE4M;pdKruv{yPt|U&fXDcklzA4weDy zmb~`@vD=^KSH>V%iDUz_I1(M_^XE`;M^M^@|6YmDz~_>$8g+=eFrf0kpy;Je9Tv`A z-wvHu31X$RCn_Z|1M%5lh1`FU3M3)I4~+^slX@g1&b*IW`3A{<@tS8OANt+yiGhO9 zt1N%coj&P|qEJfBrJ#gdj3E9#-}7_Tqk<%$Vjf6n6Big#`9#kXM&^+ zPR76joXu;ijM^ue9)@L|t9Z26ptvAlFlKWg1sOOCIL7Q=Zan#$JhsW6PRw)XJ^Q^4 zZaM9HaK@D5c$^Yu2g0N)S#=QGX94EUoO(PQX}d$0n>MlZ0zI3tzI94aPc*(26L4QD z83HN%_)(>Ad8Psy%cUw!>7}^eQK}$tBH9hi%Z9)VEFZ&;cs{p^bpj4T#94-OVsx49 zL++0yc-E(JI|D42RP8P|${-Eo65oJziSKectV=jJGa5@7ZbvjsMnjs^Lx$Axeb!$L zY^L#V3h{!0D}^fi{@=#}A_T{nB{;*TUq+3S(b}b$N>{7??B0nN9upm~VQ4%8}|}pkAGk z5EH0wZW5KmL5Hc^aVBBjsPY?IGJ0wIQdrsj29MOL0&AvgLzZB%XSfG`c*3OlxyK>>B6|3KKTVICF{X3x6VXUkg zySnN=oo`U~4Bc;47Xax5S#->^N0!c@HeW2mS^^!z_wnq?X;Stq=*&g*XTa=5C_NS% z;#XLml|FA7U+8@rwBlQdzcWyB0#4iq;CU<}X?Ka=V+EFrT*!$yxQNx@SAyksstq0( zKpBWl#7k2!HuW!blimg6ltBsRzzuy~2<#5@= zqjC`yiJfIG#-i&?CPdZqKyj~BhS{gI!0fi^6=7)4Z&?gOnPG@QK9szPO@Yk)oiXf{ zU9ZD=vrdPFWA{vTj}9%GdORFI=_t7A<=f$|<@Z8=st$Ja?}Td(`v{D0tQaRcKHC7q(Tu8EPm?jZzWy!sAxq zt;bT4EX3XY@b3lq_jqg+Cg!ZSxiFiI`*5F0CwAcWHQX-4^IzahuQJ3UpFY6>35Daa z-c#{Sc2GiCvb~?+JG`tbK}nH%u8ynD?1c8zW7s#3LVe~9PXZ{w(g= z9-9RN@tJtkTf17Ca`@rNC= z_ckElcof^~M_5+thq9+%!0l)77On1d4`eys;!1q#BNZ@oW-iqS;5&Q^EAkd!v|oS+ z05~9imG|#?_kJiRVW-Sz97^xwrxT#87&%a6&tYC96C1|Bt*8Hh+jEqgKK{@b;N{L0 zFs5#laY_IgF_q7@ft^r|+k4)86SUQ~!M;GxZhjFy^x!oxqh&Ju>8-!6SUq}&d*HqQ z`~)Ov2I>;ETK!h&7%%1ZDllVkd>fXr<>>{2jH;hM|BbpU`Kp{5*cNIF;m$`Q$n7H) zSGhyb=fj-yV@w1Tw^zqQxx{zbh_k+=a-PI<#IsBfOlVBPzpu61g!ky7n$P2|D5MU7 z!zctO+YzAr51P}95j<>yG_n^4C}QA_LLK9HEy;mM42JQy1ebISk#W!$pMij73=GC5 zzz_??LD>0&T!Ip#!yLpU5p*zB(zJn};$N~`P;MGZ+=jHOd>8;#*uQbShd+{tGk%RXVPM{jvELa{R@ASu97lLAV+e1d zOm#>plm0!2<)M67eG>NL2>-Qa4a+H=i<_gd3e)f?OV;^6Jm><$&Jg;C;AJ(mfJvT#$~ zuR-9_jG&{#hGH2(&oSw84Np3mKazrm%qs4F4tRVJukVjfhk@7>E_Gq*BnD;%W1~56 znKXDCG^Jm|zbEO*;7B!>s>Bffj7mPjpR+S@Jai?FvOpHyS!{#fq+KdO%n1AT5jwt> zgCHxk3#d5uk5)~ps>w>{Je^5dCv?6n&WLIP<}*Zkm}e=j1UD>^gc&VUeW8eh z7(|e`og0#NzX2cI@9l8hq@!S8qj~LfDqe?wzwrpvCF-^NQ|G%1GF%T6STe|-iyA&l zH*u)_Og6W?R*rrkS;N}YvoMgHfve<`aaF$+_TU&EdjMIzJVaO=n_=ikJ;VV@E!~R1 zr5=IBBwJ!)2Ctd1YX*UYC5@t7Ei)?hKU><;%z+96MGTnKAUJJKEkyt`+W4zr_D!sg z3EBtahyRH8c@aVGg$RtsBUmX}!pn#Z$O%*qG+n9bPPST}R)(uL!)&g3P(vw?8al0c zazz?^Mb{c2L~NT9CLRX+eb?}An9?{7E<5PLXhc_l)*wiHar;tRl}EM5f0(>ZUP+A3 zm2FoE#cDx;GRLr?rbWM=IUhyD=k*INA(I+ROzNJ7n!;orlK$ikU( znt8d=43`8@!Zb%2jhQ$GF_K0myc?hp5q|)jV4#87gEVJeg^uI{(2#zO8?`dEP4=AP z^l3c@K3319AWnM`yzGR=%xgSJbe2e8y;Ho^448(?kqhoLtW~jYokk+Oh>A}Su6qo@VlPjs=+qJGem5rB!%e`y^V~3!m#_xEf^j{++$;Pf z0u`3~rzQ0)bXJ|jJw42Cljf=jb}uWwgSGnRHn7=6cE*M@MBv9o+@bWHHY%1Kt=(EN z7G&IMqmr@hFp}I3gQ*`9FaTAV9t0>=oRKD=7tcyqK!gt3tkw@+*gcrqr9VCs8GDZ% zxtA2JotcLY0}K;V|Af)0M>&LGo-|pQRXPAy#oHnSwJxwxxMT>p5ihfCnmTkdtgF2Q zdSi3AtxXwS-bXyKam1uq4BRmrAtp&O(S`tD2?{{uDM=zY97OCqOM_cHzTikgO06WHW4RFu+QacAwEA+&BOAXYF*& zjcF~n?ndnU=F}4IcFv@h%swn@H$=H}Q=HeENxwLdp=pP`7!}kj|L)o7ciEa{B`UM< zK%08tS2$x#@=;I@_sD; z=J#SAITpLwJERU-i2(zJld7-qm}IUSS#W=mjcl!UWsIJwwwS7p0aP|_S*NC; z*=GL8_(_VlH&X$UTy$Ae z8o`?d-kUghWgwo7P1bfPzHQ~l*g0va%e@Da2F7C94=BmM2W#*%31L6~HexiDYn!Z% z%3PuV9xRtv9igk&17;6lDZ6~hhbvlxQYyZ22}bHD8)!j`F^T*&bDI;JNeWp z$3}>uLUixy2cSApW1)XawbUcB@lGWGN5|_+$AicOym%K8I`NYf-{QP-0u@`{WvNF< zVcvadQ9KUfYi9>fGaM%f(tIpw`=(18|RF@4f}&j;7FD5 zZCHO>o=n6B)d~D}$OEobWJ&Sr(>5^&7}lB9D|`Pqj!pI(;gTfl>&mKr7KHIopdrM>LH7x>64aTLv&S>`{8h{ya!sMeP#83fxdD~KWc*B!08OIZ)#gTc4R>!nR;314n zmx-agLdv-S%YXOIkwARFA?zGhI)>YT3?qhM<@Y>lD^mqA3I;1Q?_m>bK<2a>MkODE zmh>xlMITrFuxs~5km77RW(ULURyvXoL2YIuY^ZrR43cpOD7J^}(L6w5;rtsiYoIOp z6pzlEV9|L=%x3U4JWL5={tXBUTGG!$Ce^^DWL#yKRcCS|>lB{1)n@)N;Ug&zc^}Wo z;fQNiFsZv`1D1rAoEnj*Bj7ee-g&^1^{{c4K;V@HF~nAhW|MQ~L7yc(^yY%fE;VMF zdM1Q*kqk2hm`GVf*_`v4vT?}a>X#WkR)V(lZG#$oei%;>5mV?K?1Iy#ys0AbU?Kwe z~*#=E+>2nN7~j!5d&> z_4%+neh7ktPGqqIhLWikZqL+^S&abY8E8qp$nB4qq=EI1%Z7xj7Hb^x42$VA)lUv; z*hwB)>=)@5NW^@*umx2{>y5d+3Q6!dIWEZ|(H}io4mXt;PEe|IiUnRwy<&pupUR}6 zxvBxi)U}s=sI##BRMaXyyW4O}k;*&@W+I)fuoA3kz~r4LcR8Aj;JpgV=^F z0ABkrlT5vAwJY`mi`fDi?aEa)QdQ>O3mmg=JSohot`LAx3@MALJpgx2u4#gHk1=aG zsB>T^>>Sz+)$yuOk|XcA1L+}{*f0(b?3fP`m7gA7{{-w9=z@;=(FjyB#uBkY;+&X# zYlSR8M0F<`iv!6s_H}FTX0YsZrrD*%E_HTuG9P0W?1cGIug9_xSW-fOb)^6+%tL4N z@Pp8rdd?1mo^_*+ag_oKBOj*&cJx*j6Q$9HG5fZQuH_Nbif~~N#tr`s>NBsyhU#}9 z(3k)jlfCe@;E3)D1MVL z`p)CSX$Dl9p|QHL>)f~2(w$KaYh+Y`RVzsi=nNy z)df-t1?`1~#5YMJq5FLj$!{TIDb?>|Lxg!c9f##jz>*RI%*Qrr3Icxu0L%Vdfxj=t zohQLccKCF0zzXvsBHU=xX$H*H2pK!Lnq`Drj-}_G2UeImhS}&er(S^>nNC<={XXc8 z&$Y5M!!B-c%RONb=rMpYdiYVUI%96!OfofKOQxdOEc!r&g=noc3GuC)uSdpYW%mmk z5jQA3z4M+!=^+?Zp*%PYs0^eAp|!R}D=RfXusEN2FSXV*MG-hLAIV|3WpL6YUzTLKK@!(%^_inJj zO95ad&TS>a0_J_2tX3L`P39geGlqT!+pA83?)V`v7@vdya2GUYR&bk^ru0%C;-48M zGdJ!?G9@ME<-(#C?f_4IDF##~$tE2W@Des!txm!;Z=J%12xoR8V=AEbVYIb>2bbs& z$Lb}Bv8aOD4I=28-aIK9+7+N1U%Cxy615P=6=_Bi@_dOav7GxblOc8xRU6-(^UNX3fx|S=dZ;5ui^fkBk1#BE5Uj80+`t) z)!EF4^*amV!Gh8-brwrPx3R)?W$MV!0n6MAzzi3+VVgdNZTc(RU!~XiX>J6|R6%2h z{{^kF7hredK&VM?;@&(=ea5pu^Ju-=OPO~uuuH9o&dl)T1uXYq`C9?+dLovO;yw4b z@3{iYU$ERV0>;djvAo|NFRX4%%y&PQZ)15W3slCMOlCT(0TZ3OPL^4fbU}2EXwps7 z47Ai%MhekAw2OzkCk3`~_FCo?IJ0$HWu!y>oiMt76zq$+FCKW~zwq3)B`|N)EDll% zu`AK_{3#^5b0MzCI!3`k_G_NwO}6TzYcCoO=i)p40Jk$_#>pnB2uxFzP)maITU&y| zCi;O2wZo}c&ZZW_XN<63wGgAg{aP8bd>Y^FKe+uPmi{u<`5Y{FfLk5MGSwb~ z!}2pduwq5(Wga^$ox$kghq%foZ3j9t04OascsS8BUKn7x0PpZ;+`c`;l=CAjS9*@k z#hbhZ@BMDvKdnOJ<{-Nd4)7dr#qv(Ou=@R;`BaT@#%=&G^FC4iMkM<|_s|I*=e;ip zo+NX=fu?A3$ek;ONP?WQr!qsDwMD2htK~4-ty;*;Gb(Agmfka`x4#0 z?2j<2uFcgUqCkjd0jY!gyA&dTpt^(PeN5vedG5iIvUKbv!+9#W_0DEW?QoJg1!3vB$gwxAcd&M9$PuH{Bjqu&kFCg8n@rWQu zK~q4x-^ivq)8z+Cr7#>D%>jz#vTTGY{^SHH0&hAdWqH)_ZLcSOkmBr%Q`tCnW~$OD zh0alUj}KEzT5^EmRHouQn6>0+nFPWVWT-y4Vut}I2G3z}E~;Y*XsZxV=^pNdm=!a) z;C2|w3`1LO3rwg$gx&7JZdly89M<-2g)ctwefYq?uY%vc`hUt;=mID1K1^<5nT4)1E)wh1GEnzAbF?Hq5s)dtd|3{2-juMIK!h#G^ zg4Llef+K+wC4~e6l?F%V;&ReJvL6x-|94{8hAyMv)G*H%)La2$+Q&9N18aLW!razr zFr#HEtVh7|z2|=c7yj#$@S7#S=Rjt!Bm^e6F8zb$+U|*)B2zc2)S0^9N04_22>|Rm z-y$vIcdGFnw}qI%jaGRjBxb|#5SDMrCRGw*-lKZb2i>`mNi=RPp(O5uxLJkI{3@~9 zBb2mI^?rE#{aBXa?srP?8ca>rrtz*>`X#=@ex_qF1Yo>I0^me-`mAPKUTHA?K2?TU zbTU5gVPe^&NQDjp3GV(mpi(HvFb&U5LY$+D&>1vgWr0dvRr$RnnZsXivJa{pF;sL` zQ!?N+lpd^@QSg(SpNG0c9e2xRZ92NP4Q98^fPqXBu7B|sxahvC;pUg`fTcT@`xyG3 zOm{B(6D;dm105_&vSi=`MyrH3?BlLMI_T zA-$KE*Zc1MpL1t+cV>3Vy)(1B56FF=`?gKFbIUp3d#c;!j_0Az9nv43i|@Y4^%Q|_ zRRNmT>(Y}4xQ$JtEZ<;~z}oOVG2}Nuvtl`*87@li(-D)y+rvo)+J!EzmcR}6#}TZ5zHVlAlz(AxCB#{wdasg zQ;{KUX(R!a@zR15y7rz&8G^Zzud^!Q*i8tn1+0FJBIiA%myHiJ#`OzGVa4Isai8Q9U#hDqm(12Mvm- znsZB*m@t=h8bq}u0lMoyg=(MUiNJErb<(vTm?Q{gl2R4!LD!s&ZE=P^iT`Uul0%&U zn*GT4y`q21N(L2GUKEFhY#D9{Nvc<>BB|2L5d8c$SD){)wTx>vT(;xy0C;O9nh|FX zV`TGvg6VzZwayel_h>x+9jp(5!N}TeF56tcmw-y8m-v!N5GvW-l^gN(D;k%%M=F&P zQDIA?B?V`6UAl3AW@;0UPTFprxycL2nB!*=;ekO#a=2c$wD4fWNf`( zKswn-qMmZuEXb;nYn=j`sk(GZxam3mp3R)hAs$H}SV!Byr<7&&ENj#3vI;gClFg#t`x(NZxpW&(H|AsG(JPf`%_9#=Zw7y(s ztobkeeZ@;Kt^Z_5@}yM8V5dZxvgd?V3Se`hBvHc874YdP)YjMB2&<2nGZY%1#NX@j z_>Fjc8@_L2B=7|&A@u^+z#K)jC6~V4@pBFHF%DS1!(F5h5_97c&^%xK?4msZup20F z0X*mGyYkb*n!j$|Y&OyiQbpYhk}+2NtXD6OFXpmgU<%hRKtf?MI=@L4FL<=!DFIY` zE;`%E@6+pRd8XQ9Je`><$W1FAl25GbGb6W}lk{MhBJnxZ#C|BbP z zAxP;?_2Rvm^dxqST_G&5qEK1{$xuUA6T~Ai<%OpKmXye|G^S=GtZCTqnx6@Npzm{`P`l~+HNE3C55pm zq>?(q$Fuon+J<(I!uo`)-Be$rZeHrHfHq*{=1lfn$Am;5DG(revI!fN5?O-jhJ3vU zo^fJ^5tBs~4b#p(N)nZ5tl*QQ6M;%kDro{LG7Hjb+?aWfZz7Q*JV#F__Mx1MwegwK z%$-z6jh#(!%*Zdm%&$KN=T18vpCtl)y&OK`V7PqxcSQTs(P){=Ja^iSsqEbB=7^uT z8!Bo8l}M>>J3tp=7&=hNgDj4A0QcE#8o(RGkxGC<>F)ztq}61I?IPFceegaF3Z9q~ zd@!CTWk4vC1irEo0ynb$4DFn~oqR&0c)eYfM>w*NUAThsP05LJd*O}&4>r0vllIz8xEtEQ86aMyiD6};Xb4KNy0JbV* zC`(4pyUFlruT1aw{y+F~KEB^#ybb}Ch#LTLx}i+U_kba{m8B+b$0S8$+IWtOa3M+x znHs4+jO4)!fVlwy+SL~$bbTzE!22d$+s`vnl!$PnUeT}`Ce)0AtMF=EE1|Qe1KN8#f%#yQvJybMxCdU6PPZMQ%34&l zC*$EgzOViau@d7OsJQb!*9oNLX{XI(^D}`aLYFNM%I>^v-cwLEN=SH`3`o%n+3b)P z%{Z+0sM3GKaU|I&cWSGbdc-SZHS<^>_Us+%DcTzDB+yqZUKrk>_h)phD%q1s{$66qQ{!>{HtFqyh44OzuDSw-1 za4HK-eQ!Jz+O`kYCCq1TAe*+#!D~%nS{88UO`=_#eOY@El^83s_9{U|RLrq$BvC1j z;US2J)W|?r|=ow$JdS)(vq~jO?q62VUw}&s^)btptKHtvd@KZ&eU(% z?EO9)<@`O)f?4b5!0MKbFr;FDP{qm2eFn~AP*pLkiaV$ul@9~&978hqutVUCkwD+Pq>QJLd*-!n;`WciI$W~B03mM=h_ujIap#fYIB)8y@CiPDL^sgpE|Nm-!Z3mmSTEoJ zE^FBhFgdvn%H*(dai#7K8~%rwE9i6usqO}R`Mt-Okln`3PJQO$j4(+*ZxtD9QjQ_R zwQs`r{hh|h(*D)>+aif)|FIh#b#Nzr#tkq9WTd15xKNg18@{_*1{iRc4YHTo%Y$E$ zv}{gT*}a*^+E#T5|VtEKhL#AV}se zW>gMfWgb|jiRg=v(N_eSlaMgT3_g8V7Au9VZQD%hq(YU`Cw&bn<7M#f*7-1`yg%H0 z@Rjh}!+r+)4xSF5xAK#HFNQHy!w|4-&j}4uZHi-KvOM|TIgF$Qz2a=qi)yBOKvpWP)}1F<5tk3!v@(= zdyU1t%)=y!ed_zp|`bvh}$?C|F|(YuN2sWpj8Lu4h&0kp0n}pc|O` zl#-HCM@Qx9XGD)eMLwOH0k2>EVj(|$LQ%Cs?t?#vabOIa-;-j4Ce_rh*I1$9HywHnl*CHl^Ii@fx<7nv?9s5QXIvT zCuqjGUgc0w7=L_E>Cf--yXjIaYq9=Y5yT&q&keNf#sZ)HQLcMuIb|RAJD-^ZA9WHk zctrhe(t!$ry~WIu@mN-JV^bag?OIEMjExDX1VO{hp)+JeQf}eFRg%8B87@XZ3qaOe zgg|K&wx0}7jZlS4?6|=O#WGzwL~%S;Hm-$TrTFEFeJ+HWL*Jngp-V@-F4ClFvO|kug77AmErT zcgfI8i-gEGv*!|PPC@lHLsD|&X*%H*G}7tXG8-eq^$4Vp=MqT^%T+Zf4)fp|<{}wW z1uCsUP^ERzaYd<$7eZ8Ni>r490tJfZ+LP=qI2A0_BjTpG^K-aKhU_M4EE0ufjjMK{ zgg#LEmDR&#d!7U9TQ(^L6tn~AvNk0Q{2Sf6dD8^}G7{jbIZRKT5X(fbXEe~Vg)WDl zB}>P+3zO$Aj74&s?vQ-$am-ozWd}I_RiN=mmsxn<7KM2^3@o{3>;RQwrt)@KNtmTw z1X%x1Ria}T11#SXEd5+EaQ%O(YuZ8J(5{33Uxzgj2&gnTG5a7pIp)8^a)`64w8oAA zlUSaDm38(O-kz@_Rt9CU5)r7)?YUJZ8yVDYYTF9CO5Z5om~Grg?*BydF32 zIlt>gMRe?@)|E{V=N5v%CGJW?UmQE#P}OwIG78#zK0yoVKi`Gk)ZtFcgm?-I@>%1fwUhB?s426mRi64-_wERz}sBT=i&wgbR z;1^tgsb79%bB#FD5(= zVJ`O#4XA`{>oOEPp3CsuPWsl#SO=*XAgg4~oQ6)(SZ$uWL}5OV<$g;Z!J-qh z-73RZ49{5_hC0e{E*UkFEBZ>6O@m=gQWotIekC1#(#!bS>~I(0A~TOIf%3@S>yP;M zFp!`XGQ8XbR8;flsCI(iodgdsAn8d%(NjB-fwUDvqLL_w(r9TW=Z|bZxJ{PwSQ)Hs z-UuIVU9bxw^pP^6dN}Mqc)Ex)qebxb-o1peLzN+1%;&XbB{GuP%y)*>*C7XctC!hz z@+`x)E4wMsQIZl+5V)mo(kUOi(T82yXIG-KbO#JEaPk?-(FSDos(@`u#3-YVl1)Eh zkhUGoG|=M7yKJsEBj}oA0D?e$zyA8p7TBxM5|xk#Hpfw<<%FN{&jBjTVPZ26WG^EY z4ujCz1tVf$AEDMw9~_|^IS3SrAyFwqP*Izx5XQV-y8>B6OGcYX{ATW^4|XAhK2%Pd zc!J02w`f4GJV}_!z>r*)<07KzAKsJbu5i08>k1v+!*-j z2$cYnDiTze-$)I2MHrJH!X3gudwJ5h1(V?+&rWL_tRes1{LeF=MLwEtXzpr(Vw9?q zYCWN!yco?LwR7lSRu6A)`VhK$yLKUjK2i=Cx(^&R;$V@qFm0>iH!86oVBuyks6z09 zW?Z$dNxWVszHaM-hO}hYQ0kSPTEcUwR$*-C8~T^R#_x~U|B{R}#KnD@c0udH=n#i~ zC@4NL(E6URz_c5fa7!Oy`1}X1OTnBrAZ}Tw-U*6dQRWvca_!u^!x$@vdqoV-#R^c&Va=Zr%ty8+03MC0P~O#$abz*Mpvh{SSNEN5`i5Z z#c@0sRyjy`{e%(W*VqS@iAs2S*?-{ZQHSk92w2*C+M%td4F;4CC|VmP)Qy90PdORx zUHBACsvDb0hRNM`td3*MK|Xn*O6c%>Wn^{8W7EuR>!n}VQ>${+ZJy#kl37Rwt>l(YmWwH%>(PoFaN&3@) zUYW&tz_}{JcMSI-et95hkz17|8C(k%NReN6dgaGsARj}8h0qTY=YK*{J3Q_P72XV5 ze4*R1{=jehVtB!IOxq=t`W2fQ38bu>6Aws`yav`zRTO1W*w(odicu)^eB35^9aG#n zn}6ppu)H6Ut7bwo zf~Lw?nQ!92C5RLcK!EZAP#mt=!u#g5Ar#5IR6psjxO10nmKj6+mEx@ozvX?u zm5t)g6+e57<6D$vn}Qvt*Nb9*exGT!Su_1Sw+Q&g^-1j&c*+V$ybMBRKu(yuC@0sa z^nK&eI5c)P6$hwP41~&fxllg=sHY8Z`i;Dq60rn4wd_BiYmhQ~?VIrJm#%>I?OS1K z(^`1o%hR$3$SUa*R8;*kfegW8F1mS`xv4HxvI?sv z(=}Y1jAI`KQ1j-~?{^v)l0kX_4L=?~TybrCxP4RLwnMm{z!>Wle!_)tE&o)^k4;*d zg07d%+-o<%oFpkhLWBnq+c^yaCG*?fxl`DR6uOM4918WNwV4rl3@+~vtrDJQ zG7Dce_m=D?!7Hu3?Al{;bYV*$ojw%EtW&{bvDBfD$o;NcmLa+^bml-unfx^l#P3c~ zH9(dH7Jg{5Y7jFfF^9PX!}hxwW)dKcjAQZT6Ye3Ea_BoUq8@3m`Ee8g1X=egV`%d| zt=Z$SUSs+GHgE|3=P{S6vHIFUrh&>DFG-1C@fiWeDz_NPb0!}ez zB9SOu`NqvfNtuu?wFpd47<&|~Z{6e>t=4P4hwZK7mJL;Q{mJgi)9#&p7^t);?5wLD zz9%&Lx?7*hNTJ;%U~V+P#+KopAT-=>U@q>1j>$?|3YFi=X;bbXwK&S{lH7ew&(yzf zWxAwJ1yriK>?=CL5D=My^(BY%za~p>EYEW)){|6?Y17p6lOUNpL9GLP@8bOao>l$C zpb44VmiBE$p@^fahYMp*zfvS8VoJuE!>fkCtaWqX?axVEmDhx-=W_VZn%OX=ems=L z5|Gr5Rw;(b@XEokvUwf+YVPetZRa;8o&a@;YLP@zf(hm@r=SuOmE2q^b5BKNt{=NH ztUe9hy~GmqlDY5d2fHwb2i&AqTyod!$H>{{dj0UbAFHZD{7PJ8eq%oF=@*!XInO)_ z85mzKyEZM-ukl=qdP`+zzMerA2pW#PAGA!=J6)tz6xQTCpYdIkchqU1ve0i7O)KHB z3)?KS=-hIj*jZq^xps1g_K-0xiQy@tYcb01{U+r+PY{A(yaqiR?cjg)_U#D9ieraU zi1N{vkKxjp*TU6r{swk*H4CpAVOGq|JxW2>Ce)6BXI8ufk1Tq+Xzd+}fax0(z5*Kz zwklqdOFy|jxB?IfVk71i2-2l2469E=j!H{qt<&{w`gC`013L8jn9jId$I^{Py3hlX zNd43a7&!F*Z?X3GBVmvQQ!1{iR>={Qyf1SBsoW=km7%dt=?82IC4v0A)3bPh{2$@x zySPGn2?p{uDP9KH7F8n!9y}QI~!w*v3@Ca!~K42RF+yNluYarw}tJs2>gl~ zWyOKc=W|%wQXHT%wckYOhhU;R+3VRKGK7U$q$&f;>tRLXT6kc=lb=bDa{CAO!i6)h zfw^1e2{jsdJP{yq5)+f5@rdeS@Q3&Bg{2L}vsd}X#N%71%@ zuA2Cp!-KJ;QpfQ{TCP0a=4< zRhtzV)sPI&;4VCa%lE4kxUyEQ|8o9L1?N0aUWL*r>>;}zxx$)ISnHHD?f|TFoX67k z?|WcT>q^ol@|rqURrRw%Z>VoVr^0x-A`#|_yF0gQCZZ*

=an7}juPnxm?RLrYJq zUooR=UlP+ERWkyfUpWJsyP7{U0ANdd zfd$El;w1vBh?l`e1S-i?vN%$e`tc$ZgATD+t5^Uf!@t) zhpy_T>*b~9CwZ)_XO!RdqI#^4LSox<3veH09xKrviacE5C7E%ki}B@g)j*asH}SKS z_}}P?usDd+13V_J7Xnwv$mqM!^~z$Uu&I5k04v2Pdkxr4$YZ(9)5#JnAZo$QfbW3t3w<~XN7QC_iIRHc><&$K9PyONOWN1%tsmy~#&?$O_*0{F4d zaej@ai(Z{1J%0lm<7B4@uK4~KJDVDrHpv)ghe4yc#QY8OelkGc=XD>_t1qyovex`c z8d<=$IsE=ZS&5sauTR4Fr^?!>P?@jtATkj%y(WcQcwj$9doqF2$VIuKvk6u=hmyZb zx$He?cNkVV*yIHwGpV@5E-YQ5WSdbn6mEX^?oVB8mBl-j!KE{9DzdyT}71+jc)TEh@TsFSUfM(Iazb zg!A*UQw*`vVtiXc3U!`@O5qQ>WJM-^4C#U&!ux#@UylS$n&SGrSbn6y%+Cm+%Si;i zg=-Obx1=l6tMNOoS8%D!;e5-rHi70oE)9-lnRfutZ8*m9kEALK-D0wMwSjvuc+~)J zPAO-cY)zeQa~weu&5N^rO0FojN}byQF~j>gc3wNOKIgJ|c)apLFc%QxBw|+42hy<= z9d_Z4rSOH}2Ns-RRhLx5Aw&0urz!3iAv~7cELq6uZ6q5 z_)FMh;BI{ypuD(pCj4jhELgN-1+*cEn^ZSWbOr$wuZ8TEP9gnD*BMhY0-pWkWjJ~K zvBH+8(B-6Y$H2^Wb0Dp_VlMOCZB8>#3*aSl4cT!1cI}eLAUh}QzRZJY{VA-6%giyR zdD2{k^*0`Eq9k;rZ#)3cIf7J16ha-ejKC}K^FsXm5xy?N&pQ->+OxXIeToFbB|}_X zP}$&QDu7Twhhl9_F=AJ*IV3a3IJidXrmx6V_H5?B2mIKb*zpb3>l!yf4uzNtd4Sb} z!-ia@uY$x+l5t2;N$x1o7K+qMd0eiLIoI&aghSqPo6AOt%7=O!|AL3Qcj{QTou8`r zfsxsl#mivn&f>+NI-CF!H<(~aiX&yhs;3JjKpa@!UnDZQSrj0Q zG9_OITF#d_G9bla2mLU}(}yDh(sJ4{bn76iyFiuhC9RT^kqU4LIxF8d+)GM!u@~Zi~0k5oi0~TysT<~Y6aX4_uUa&(% zMrNMGN|mAbjhW)4mQ_sW?lea4=fkcf4~3#Y{!ivPS(bMG(by=Sr-r#)_DwftYYAg8 zO_hFHev9N0B)UCoA_$RZp6a?;&q5DD!j(-aOR6QB|fEPDn#R8r$+3xq?(^Ky9>Tk<;=i(Pb zow!37jKPBju2a}0k5&b&)^(Uikf==On9|jpVXm7xN`;@fRI(3R?M1Ity|UDvQjLFv zP)MS3tCj z!Cf{}lA`zlf+SfPhG3v1Rw|PB-TMAtpsl;DV9)x&mU-~IcmD+Ez48;d5nmTJd;-I( zh6#W|;?tA`pr{xQt7aS(=>{qX!)xp3iuf#rE@Ue-zGf7(bhj#l5mj$GFG(F&SWmM3 zYXgLehm6$=WgYI3p;LzEo9)ycg>q4qQR9J4gj(78=VVA($ihPETiNS`tRXnsY3*|c9P`g)4lf^I5kI_ zX2%r#?6mv4OPa-@sbw)yH;{3BP%L^t!)W`z-S* z0Sa?UHA-<3=e z=24`L1B@!WnC1G^p-b@K$ubcv8OZChWu-HAlahm)2a?yl>6kiX+Vi(BfekGiMdsZ? zmzt6qIBnwb@Z0zA5lQvZ3H3mN`N&Rmo&z9KLhV2vHQPHI;Mecn4UZ$U-VN`6=&=1o zP_iMFgO#6UXXj1?ChK6{w#BfpVYy(!6#aKg`*vZtJfN%}46cX?X-3LMk?%I{UgPhy zn{>6SAgN0A5O{gb>u}cOlVL*rxPp%TA;b2Ar<>092@)I`IT}0)awPcdH2X z&Oyy>8$jK_H0`R}ucQ|0v9|QI35NaRn*YMk@`1vImx8O2Ve23S7!+(R9*Kz?kuhp_ zZx?LuXb?bWW9wGf(6$*iwQfOBvIAOsJ4C-yP`$EP8H}wNm9dOZ8GxAA_;OIs5#q$u zo#q|I(`oH(hv!zl3O_0^P}y_9ZZM*92(>Q*+?d#|KH?Pbpe})p*V?CdMfQ&T3vbj0}w7LN63s`4h zeIFFE#u47vGag{mokt`hzbAuRkXAX+(3u4Qny6N*-zJYZ+)_xM}C$OO} zfO@iE*=s8F%kkVMlCnhqUx@W_{C>8rM8)eOOkxKpx#N=F$=TgMC?Y32mCfzjpqS;f zNhiWH%U>`_RaB6^3PX3gb{e#hHx$*(@dQjnJQMvodYC85SCe-3^UC?+H#0OyNDjPq zRbC6UDp3Ken%2QPn?8guj5w&E<37259K5vVe}Q*IDsg!H5;mX2v4d$roQlfi0+bW* z89wv^!7j9GnqIHOW7_eUkMaFN&^#h25Ev02QpZq?g0ul|oXC=1Yp{-FnxtlOx^fTN z=(HY~CV>gWc$v?+TBz|0dc6WlgGfs73`lGY{PF_TvGC*Rq~MOf3Qr)l=ffXX(e1%KO16=i#gqGf`)xh=e~_2~pu);6z)&8=HtaOI$)0F~;JDmZKM*WlOh z{RMXGH!%~5mHV7*Dq=#fXowLO$%dfx00k<5fq|Ock)K8B6uH<8pqL1VH+S<#1qUh< z>&6N-SxRIdW#G1e^RWwLKIVnu*u2j5U|pY5ph6Oe3~}`Wz8-y4j`aZi{Q%WIm9-i1 zxE@J#mf6t-6`BzyBjO2lZdSO#2Yw%-O&ywdD*GQHGdy&+;})yp-JrDJ=Tq_jlFMOh`puiv;3PQh_=XG}f` zCe(}&v0X^bBD=AK-VFnAFFKYC?ZeXdnCRa;sE3wJEDwV}ypA^V+v zrL~Sp3f&}CDmyUdV{Z8U&GP$<0_;rlT6N<3P|yT!Dmj)w;#emyNEaseOS?Y95_P(t z4Q-8v1uhl6QdI97b(gEYO@+TxppLuT{zq=@bjcKuZeYb$+7L zdhp=mCq*1O`{gi@w5tA)DKJ3UyhKha1MwZ_P@SlPSJu8>RG<>ao%PQSyd2uQ+o6kM ztN83Er#@Cun*~*`d0FLqfeXJ91eKkW@fy{M3Ru&!9+vO?q@ZI?ZsMb=heK;mn>5Jw z0$wr{i+M0o=CHu{C7Xaw>tn zWZM^pZk#L353dZQ+Ukrzdv6zPXe+#}(x|GT(B9Lj)Mr7%ZTNn*2XJU%5)vMRRApzL z8TXN@2F3dEG6bu`OESaF0Ix%xHY@)k`0UydH=0FZk@03$TB$lXV{r3do(-p29sT zlH>m19>?Z-w{>l~$Mx#TBh%S+C|(fvn}CwCCpUuxF96M@yxT(+^Afk2_bzk)WfcjK zkMUo~_2h+2eqJo_lEmm`j@!6eb8-QIvi_U<_``Aq(-R;|(wZF+7hT&wnUBR4WMTT1 zAo#s*Iy&m0iUIKYhIgQ|x1*?(uryW%XHNPmtZm*XJZr4a!@xVEbQpaq%kZt%+IHtVC-MtX`QnDNE@OCFTqPw6|3lb_&Q4zYF4g2Sj;0BzPml zcnkg(QSEbDm|vEd&*ar~i^TO_F-nI#)an$fW1ipfR49MO`Xra1=hsWFgWoH(?Z!gN z$++CSOF!G6-Db@*vQb`{Pog0MGFTqFF1@J8PGLpU8kn{A&7yDHzO&ouaM0lCu&Q~z zNT6rzV;R)0Vx%Wyh%6f!QC79U0*s@KG>&@^ANzC!41r4FB`PF6D~*=sBx7`Hv#hOA z7|feV%nJv#?sly6eeRx>YY_^$TvO1l%R!5W;ftQBwF!^tRUJLb7e#J)PWpLDCV7Iw z^-f?8(gVX7Hex-sfb;5fa8R#t^QP<(eEEkYonD3Y2h3Yqb1UfSbL6*oDFmU7IL-~? zRr8-z8q+^veNp1L*;wz5c%bd|AhBEJa_Ce}!lAHu{V+f~=>+kK%0wl+ur@U#fC+>UiT);mFW!&b31-~s8NjfCq}to!2I zpZa(fvcDM%T7a!T;dSoD*YD!B$LS13dN#$8ag`t#X^l>X)uorhro<`G9vKG>@dIIX z$)&I(emF9>omjU4s4+Ln8MPbMZ(_ZTX>Gnr|L!uy=UuLc-&o-AJ6YCG2RS>CFbf&i zz5vd2?Dtr|t$58l@SMxAb~)JL{KZbHW?>HGaG^&8$i67}-O0~g;C3OJ^uJVzdlBRqrnrDv2#el$%$lZk@TVhx1qTe> zr|5WP{?>)?-C5TPbr89+CN1u>ss^S$V`2DWd_2UlQ8~wN{Es!w>*3Np&xZ4+ol(#+ zUfH+?E}eB9L?dwlSo{LUpfOk6)|HR&+n35Sx8Q|~T`_+A2@}5TZN#!6e?58ptkp2s7tk|Eeg;K_nk$Z%p+2GKPcll85mTk1`{Dif1TEMfbsy!-c?-V_l0}!I zEFX(?C?zB2nvDy^3YsIk&Pw=JF@`&12P60#4Q-L}__-3Ft{p0qi=nD_AylX4LoC${ zJ&}6x{9a}+`ZykeuY2L^7`<&I38G+&nc%ZtK!rMLbEpD?0L)7@^Q6i|rr>64TQCDtPk0MZ-va!;YIYr1jBa3*6#i4=5=uOewV;E zCw{e{eXzD=16(rmIuZ1(G+N>~ZiOBnl{IQPHmMW*jzYCG0^PuPH#Ei%Lby^TKKm(2 zNFbwahT7!&0`&Am`r&saCcq*&Ps^C5hWI-EewYqo!rCl!>7eBAdZgLaUh^csKt=nt zgm1&XuY|4fW1%5-D5O|Is03*D%{fC(%XB*|a&pE!cci~Ct*z^Q2L|^%3vulCE;i7y zg?R=lpy@greF$+vJT*5SG{cPnK4Njl)B)ZS3vGY6`ZA3-T^klq;@3O)bR zdL{b0FVmvZ?um6OXrb_HB*v2h?9k3~3GIBDW+3}Mm-F_>S#>0uk3uIh(z+Y~=65wy zUnj8tTcT57bK+#^h>XC`+i@Hapy1rr3}?A5ut_3dX^0(!JM)n+tow1ONUek(kt-|$ zMOEneTgk%qrXxBU)+N4;&ooN(H38p})K+}n%}}2F1j+?)*$hcmE+iAh*&@or6AfN! z&(8r4_vk~r?|1eBI zu;RsV!cwc{YiFXGt3>4wI0X{+Ij-lLNNam2h z-D+EOB5Wx+NiYBcDt=Wz=^cMpWC*M;ITIP@0hyg)2@=|FL1^Qk=)p-=qjU9|E|pmO zV;!u&FK1+Xs;L?}bbzv*Yn#NzpgUobR**WPIl2cj(h*SC`<4*l)6U=Ts;BD`BTgU_ z-Vr-O?6L{8l%_U_-@H6~xNeMD$~Ry;`oYSQ@589>2StBV|MP;x9t%_2uvKT|m*|Xq zH3GGmQ2BI3heI2i2s@((3cymCTnaVGc?euqLW-3jII9gsUXljrJWv2(4SdFgq=XAV zOHvV9V{1z;!1l()GuV$$+D1v6vK?$Fw3nO(EwMcj`2P=|dyyFP9#-#bd}H1NA&gzf z;3ndg1kNwxlvM%b%k>Y8QkBZVFl*f$Sh#&L?ACuupI8hR?0L5EN_uwX44Bq`k`~k1 z;faCy(FcOdhsLPmHlwyzup^vuFG4^nPd{1~N)ppown)xGnD#GxYj37%If>~#FBT)G$aecCWN3hiQD zkeW%}Hbw&|B>AB0HAkmGYji4tu1aM3?FhEUBk+m~sfqVBvw(%AnxQo^5n2-ypenUc zJa1KUp#VYM*cYH6J&R5`!_#n-l)hxxGsTN}1leha6|5|1Auk*9wy7(rh69J}-Di;d zQ6mn4)s1W5jg9Y#M24k$LOWxh$mTs_A@!@QD>*AVxp5xrm5poROCt}3^hmxddAZz*|Ok&GQ=fjU){SI9N=V0rud$Ay5&8w%B9@1iRxk1_-cI$~WS-9m490<`W0z z@vPdnq?YN8RKtdnv#|g6)+B!{ChSKDENF8yBEGW+v>iBa8pIrGjqV0p5?_Vwv7?2A zs0TrLXJj};SW1t96SZ%RHG7git~Abce};3D)M8E1J)tW)Ld?1H)LQY})E6YFVYV=~ zSAaz9Enug(8`GxC^mNK|8C|!ZEU;e3%BY?ET-Sb=-3h5w5*|MGHW3W2FO~Z~{u|u8 z;0cIFOJFFrtv8hn{LEpT7v$POJ;nUY3kpZo($fy@J+1KASMG!nRl^ECmgHr2@vQ5F z>WMNb2U!bY{2pZN@!T=_l@esh7Xg#T*uJo#^lT(#t%6AhWnDQT_qhI4NTexzu@G6a zD3qnvi2D-(C0h#;^AoeC&%jeULr4hXO9r|!{VF&AGuCwYyxmB=y9Hp-_U)y})T;zD z?Tic(l9RURIOt*n#BQ4iJ?J(C5ytWO5&>jtk{`n0?tcjt8ZpeYTFc&;HX_9SWPh;{ zK}rWQFmf{%+AZ$Fyby$EjcGx5u3}p3AX7%X6g~q<7W!km-iP7cPY5R3B~(<&g3u|U z(jvx<48Aow9)ZN3!tSOE!5sk>O{!up$ii_vdNf9k^CQdnm_cgFvaym_Hd=2lPe4z! zAH-9QP?1~$Rmr7Lky@qC>CTLlgz4v{3~C*cfKY9TPK7P;6A@U9f=H@cw1MU#sny7k zmn1wS#URk7NvM)~8zY(#EY1VEMq6|&)=2`4$H-Gl^f@yDOK$Y4Guleh@21b$an=sy z`q@zd_6Vp@n<|nkger`J0!HwD>HK@zvnGT@hvqQZ^LTmv7|Z*3+r0~Ef$d~xU=6d* z1ArsD<7ub8q~Q}dY3$MPi$ku2zE?>B4{YbQlc8)v*X^JyYNvGq}2q^9Hle|>y1Q~)DGUg?bI7u2xktxKuE^!gB z$jCVoe8CVZbQ>4$(Z3ARsv9EcTKM^t5V=#uJ0hs46TpP*8#>uw0itMk--Ww;GW?}L zYcYoBr*xYg10&Hug9g5c0aO2t>bgfTjE=}q9l~=HT}|m8EyT1}ms~6u2QfhHT26~h z4X9}6aM7;L=x}V$G8o-?xA5ZXLS||;k`9;Vqz!|&3rW+q_|YOz9Z4#psaB!dqOs;q zvW>Jirim!J1CC}te~oZ}N_LC| zU=hGS0%ghd2>#cLbuIdlY3eLmgt@ta55GiMm2r9c$ZgYL<`Op&EUAG4aN9E%URB|7l2yv3f1#?S9keH^DB5XW~@JW(UmRg74Wxa6K zElqCF?J=sg9b%5#vq86y42z^Aq{r_OHZdh)he^a;11h-7jF1gNned(=l|lqMg_K1A zBMUgm1uEJz5p$+%QG17X{!Q4ekZl9Sl3|&xPZZm|MYQ9S(jN<;M#0*MDH^X84p7lP zlhmh!4S{mpagXh~Tb!%KT1kZrR5TkD%>XnCEz!xaJ$59{ttrUZHKv~8%v;xF%SdE_ zihiO1I}ssOqWPpr=JYY6IiBhjz||_vA+=kCb!qE>n?zKZ{!d2A@z^r`7#-_SyKt_M ztx+`9q0K)A=C8q zzl}@_CY5KEPZXK{T4=zX?z+-*@%_e-ukR12@EkBeBK4S%o=`9|`kFvQDefFAQ%j+$ zcM+7QR%QS}TC^Y01E#e{$BKYjEy9?wTmS-MNQ@=Lj#INo>BP^y!oP+a*SZ5ZaRMsZ z=af(tk%vdW-v7dY?pZ=YM8JfMTTAdg{kFRFVgyekg}W{7gfoC64?sn%1xgee84mTm zb75rHeFCWT3R{|#)ciCW{n~rNaG|oHacGNY=ONR#5iWR+5(h;(kpk2LYxZ5ENd^L3tf)-|@ zh_G!BxEi*S;bsimLhWdYO%+vG2&8sE8Q&m4%<9tbLVILP)@YeIT+5*??#8sa{hL-P zkyNTRIzgzo$ZmxR+l7)$CT6#BBPkYn54A=ni{O1`BUsHSwmG_&uI4#HSA*%sr;O!h zuUEBrwlSu)0+sAJ%y}F$=X3qJvo-g=%wsXz^*b~D%xoUiJbtpqv<~xwF?VXin5DV8 zoy;ln5)~>GTx5Z)-AI@_as@H8s~SR8Fxb@H0;yCl{OiQ~p{fY6HdM-{*3Izz${8>d zck0WVR*NU7PE?5)AyE;4iA$?>Wc%NbJ9;7QT!dj3BJ#~`TVY2>1AJ}l(eS&&e+I?( zMFRNEJ9ok}D_#FxHXZ6d6p5MBAD_NeUCH4T{;Zw)6sMj2)PX*U>j6D$WuN z0x(G^7cS0m?6Q76uS0$B|3tvFt?^@o8iwo$BEtJ4nl&zT==hx7uMdeznlWe}lL9c2 z>Y*&TPJ~5okB$?DmSk{CQDv<==6p$1j0TtlB1qaZy!$aR?qt7^-gxBB&Xkb0QS{Wc zCFdjjsum#6ZeEe?1KGfkDk`c9 zu@^3^P3z#mL3_ZReI&HLSLx{KfHyY23oCZ6f_Jyf6RHmKWU4~YQeRr5+e2{2{!p=9 z@tdy)d@@tn>rkS>?qrW}HBTbYJZ#whaLnk#;kYqJ_K7hfud@I5>Q7*G%SHr})yn;+ zYF8u^qsE8v^38bMwfXHSE{q(vCXU5=g4pRp9^z{h0;psMD1|(~Oz7rC)J_qlG^NMG zUzNHT$@#(~q+7S8$!;rC0#G%^ z_JNIw(}X<_88kZuL351LvJ0s6IiMmFMTu!dcy&}KN=JxH-k?hpOz6ivkj47+t z3Nc!-xoH(@uR?-zj2= z063@%>;}P3Oz%te!*VPiw4NuI+crHLu7YS^;+`_9AIqv1QW3I|p_n$LI-~d@2D-0;>pV_Mb`16jF&5y+uB`x4Cg`rHY8}bDiwD9kZ z!k%Dr;v2$VBO*ljk$esdAzAYGq%fh~KCh8(AX)n^!}^}vwM61R`>fBDeCj07LhW}4 zd@gDexdTxaUdkP`IM36Y5;2mQz;L}U=>ePcO)lMktsf{(F1FUz(!RVN>E%Jy=_Fz;NQ*mL^KYiu@VF?afo9j35@1F!=(Dqjaj;>FH*XWnmm{&s9kTW2fC6}yYaiEP@TjP zP*o->MB0PVRYPH7-B_54z+_s#Nsx$@^yzVUZ_9iri!N`*5Q@w-W7k$ta=;XlUu5Y{l=8TZ9{8rGw+Ez|HuF|bpgh&UOXz#GNh zc~WnSGTyd zO<%!l9c0?%+b%A%YEDywut)IRZ!6sM>v-CUc)c&+>zItnCC7-AXXEvmR4#A>38eA0 zLH{*@pP$DgzMNhI46-@0a)=AFs(b)UL3b>%RkFvV)livvJ`4YU&5w;r`ZCRU!OF0} zNnc8)=XPyk>^C0#V>o{7m*BHjQV0Mxwr&xIr|Via!TOesu&I3;Y-`^RP2H`~g0;1` z19}iJbf+|taFj)ah>!lFLX8y7eAd7Wv)m(@%vB`Efqj8Od#(!=8RVy z9LslkcS$A}*`2XNV13D1kf7jV_ELwi%5)^ETI6kuJ%y{^yX|*2`ugno1XeCc-^VDi zHIzC$XS2>2GS^^SYgg93gJl_-Ya~pYbfaq~giitH35VP9|MzYB^dTM0diD!s8EqZbpW%1^ z4+pSHijn2mmM|l5`8oE@4aWQOT=(SD@$;{Q1clq&Kqihu_A>#>{q#BXG=4tSamQry zUFMRGuDEvgF1FYGhhLyVg+jLeMrH@-w!US;dCM5;1_d-S2A-rwq5LdOolWrgSMG$d z6czS!P&fjZ=I%CV=xBgO1S>l`cEa|qMriA4hYqaGU9Hfb>=vq*u4E@9g#;uiY+};! z9JMiGx=oB`8&eW35decEHI=baAss1=l|faaT;y7+OjL`wFyysVQ&IzE@v_fEe?GC~ zS-A0?yI@lNxD0SB1jtqe|1O!)&%yd|KKqEo`Rfzk!X5K|B3fsj?NaW+>tB$wGn64d zW=3K}7G|cEHDde_zyB&SoDD{HYbclEfp;?F&eG1H*3MYd&VbgFsjw6tdK?!bb_Jj! z;`S0(jh}f2@YH%|H;5QV@pxXwhOu@Hz;4w$r8L4KY2wM$!vjE2INtHR5`h?V& zNo9+CoA1-z;12SvU=7-10S1*&^!Gs1dpSz7+&;TKH}`lJfMC&XRe_BVa3NEvMdsPw z(+S_5c`e-crQgEH>fxVxpu$*G7=6|#dZ!S;;fBQ^BrPlgeZe)jSfwl31^-;}5)3Hs zZw}TK0DP$CvXb9iny;^aUYu``Lb-b=$4h5qFvNtdZl320gBmjdG0DQ^JJ2fLHb}M& z{u_JY`;W1H9S?sk^KF(P4bj4kgGp^;lJqdh${0QP4_!9NTsy#*~1m6Xk`%A zKjI1BFwYCCM;XrryG|e_D_JSynhi;}#pw0dSg*2;vYJ*?TIU1fhIynTpu2RD0y<+Q^fJYg0|3iDw8ESO~)*U-PeIiqS%Vke1pBGPUl!NEnM!zJ3@ zi_CXejj-}AAv?erV2KQJi zs@^^xYx2B&_Yrw`bVY`WAZc21wtR~Wq0`JLEYyG*RW&TrNsL>;49gN_1A)%klJGQb zuG4kz!?SCJrE6EN0g*LAYA%rCX`2~tL)4fZ;brDyI6R^6p1mf^)p+b<2#qGVBWC@= zJPXlsW*E;=d+m;gJ%_Is1r{gzt4ZL7PRoo#1}yd(#>wu^WPm##PyL1o=G_qs;8rC)o^LiCOUzHt43z)S^(trVHdHXZ7Y2Lwg20NNOl#e zLLN$wFL@3IRSeRNh1J)LtShn`fZ-CyIsE=M*135HxX>%|;n=5t&ljj>W{H5Mqq1>7 zvNCLLbqD3?96Btw!16f&-m`_A}rCAn=d{m)4B{l&sjBOFiYM! z15F^u1U#HYT#hct+50r&96iqnlCTZLe~&gx0bLd~b0H$O`WRsVoc1{TdRArR%siP< z(gzm9Sc9~RyT#tgZS#e(5D38~+#w;Emox+1){%?E&?`3$_qb0Oh3qfJ*N%d@o9DxI zZ`=$w9(v6#PQ0tQmM50{3zjyng30ycb)(^6bCO9)0Yk{=lP=#aNJk+Rqv)+#Bvf9Q zTUrebvqXIzkee~HW61Ww&bY|f2h;JlTC)^{o2VE5-v^2FS3y^GaQllH8%Y@p=ZlO2 z+=vQnI0iR~HhS?ZVx&{ehj42j9FISY#<#0&X3j7?=XP7Pn^9A6D}i1~yJ6k6c zWO{Z;>Vse5`%Q*>lGNs_Ibw_);F!55xf3tN_l@{olhqg`bq19Uy=lPDf5y*772F*B zeows6m3YKhC&Hqs9yz%+Pr>5`WmJ^RWUI6lUl)O9FwQLMwKQs@068o1&289@4`Ly# z@frFq>p(AX5hkHkIabAHMa6D~cEywG#==v}Ux0Bnqv5>W&)CI{cNN>RwS7C>zwk*I zRyjo3P_Pim9_@~^tl^iGn1;_1;Ymoc3ZeR;?5V5(IEC914_)`pIc1E$mSx>UB`+$% zrhW_G+=?e9%r`Um*l_+!yyo-Fu&XxPqL>VG-@p%FWm$$`i2P~*PELlsOwefZUYTeb zX)|LZB6hP4F?}q%E9UlKN^3sNkq7YHm+{zT7N9i-+j<;6AK5le_S{V}yapHQsBWo= z>-JemZ0Q}@%cAKBKx(7X&OW!y$HTwH4K{}vgI+6zVQ>rnyTUAm%pCG}e7z(Gr0VTwHlRI} zzywS)taTMJyGff^0SFQEnA6OjYcNQ%8O|RXI^-oB4 z_dr73VcMaa2O4*&T1t|%8pu3YrWhnq;DM0aT)f7<*oK`s9w%9ba)1z{#k#1B2S`Mw4{O z<{HSURl67r5aj2XFWQ6HeSPt_*>hW(u2WK2Hfl3`aS%0!4tc<@~&bk?{A(5 z&##&VqiaTpV0Ef=R>p>ed8{)I_L6qDIc_+H2=Utd_Y`sBR%nlofi46ql!3UQ#nM3m zy(~i0`c5%o2)ZRtU$ZtV=K$A?-?t`hGL4JTS}ykT|JmxaS&`o7;Ab!tGfoP#VSk=vP(`H@$l| z{Px|uc5&xjMU?IB+u?`*{ke$lTNW*mG9u>+^uSFb$Xx)7?E27uyuJ{k4aQRl$LnK{#Ks3POf_-sdvuw$%I$oxhGWpY2aFejX$ zci_KUi$d(UwJ**6#+eO(luUmb@$bo%Q^`2yQ5k}(k{>2G0CnGeGD%5{lRkY7m?KW>I*d&W1Xyn_Yv}jFZ8Jnd z;Zzx^eIos!lMRI^Z`JKHd10z$mAgtU{HR|Tyk$Gs8Yqg{xx4b50CWyuL7gCeIdjEa zX4ngmb(hBPzw2?$lR{W3$Ka(JEgai8c`tJY7E)3qH9erCO@iU3+RFvGjKP$FfznJY z&)VX0#qnZRo2dPofFJU}{XtOxiV=Wwuy=*dbOckTPN62xV47Dz(r#^d>Cu_X z+>!;&ZoooCtZoNHz(I^|HLj(ItCQ=-!Luu8z$G)T&RgWsU8V1rpZxciFn`B#7*jJs zMCx?{z`Q{1VT!+TEs@-nh3c;uv_V&7m;fsgGG8#ntjKo-7b8YfKiTa18I#Ji+nYfJ zMkdV6Az7q?LIo?+@j4ghc(-KBdhKf=JvvaRg6xQ*_U146EQ?K&DSxxM8*le;POhTt z$I1A9qO*Zfo)$`u*fMLptgA6j&FzzPekexal>ZQHyTBoBWUT=ac7~HXx-RKED@Te{ z0HR{MrqWU)gH$lb?n+@d6$x2-OVQ81XD52L>c{=ssW@y(TxpD~QLikJ@z_R-K_VB_ z|89I2E}eOu2s6K{e7?#JZ~Y!#TKl@lx}8iVLzQ&c&0_}Mjt@TU#Lg=qjuLC9H9AgY z$R#yUK`dzvB70_tabjdZMgq*aOUw(m-scq6)R2(PwA>V z?}xR@Qb~pKT|ECC083h71HQm(&oo^B%Z@}=W86Q@l}<_0@LcA0kL^Uvj>VUhkhc|G z%1ZV)_x8TX^gG)#H+;DsON8@>^?|BVs2;XB{BZL)m;mp)c)NG4ad7hQsN(R)TwIA< zRc}t4nDEQpK!JhM?q}eha$XgKs{k4v;C$}$n6y6!A>66;6JhS=kKpW=E{7Wqx&o&4 zpR$XCe@;Nk(<@#S-eAu8;QYA@a~~URpZ9(P>pu$BXAu$Ptx~9cBD@{A(rOJ%Z8JpI zF3F!!4J6ffxdUE`Uz}iyyJ9}3$#Kw(F3ONWO2h*%LK;K%Bcnt%_N&i&Jd7yl1v>YFyxoCK%@3i0nC^+8B-zw;vn z$erZct=wCpU`?**xvcF&0K`~+*8SJOJ12s&^vwtDDOj;%24rMAp>8y+ZP^5u&H5Rf zIr$_webR|gt(Y@sSNU|5R4NHqzkU93uP0!kII z#CS8bM0bbI$Z&}Amf{T}%i)kYxlI%cxnlkssd|aa0KQ^oc*cldxyZT2u8!?Wt72|QF8q$EW* z#6-zNgw+n1WDT-r$;UL%osqFzgILb1xaJ-71-))HoPhTuQKRE7<+Z}B52n78H|92y zAb8vcSVr<;6^DPznAYLG0R@L#I>7=?Nfz={mXWTIZNkW^Ay6K#fZIQM5We-|58#EB zGj?(H&sf=rVDy}qFBj^bN%iA1AO&0`Ak4?Sa*A~l6qs8iHK*e1=0a&+w0kBIs7!$* zE5p7^K_QHH?M2S!jI|kkyPqu(6U=5l)}u_jOw-mID2wuJ0HZAe80=S=F`rL|)&h2w zqm;nT^MbP+yBM3seu&@y+@R_r`^Em|d^aXW)F>qi{TeG*-#pjb}r@MB2NCEgQQA@@{L z1#Ruv0aw0pGkmyp0sL^Ei=ZrCwu{Su%FAnO-+&w6xf7bYTZJJt#Y*84QbCm{$X3H! zr!*37NjhcHPB1Nh*MADxf0Qkm5)igWMnR0Xn3!`xOOA`FA;&GxNEpu4U(?~u)1au3W{e|fX40ThG;9PiF%bD(AEYg8+F(vRDqgWmisjFV1}6} zB(C{g`d8rn=E%fXQIRAuP`4rR3xeG8dfBNl-L6LrY(QDPh`B;G3?FV?1pjaM)8N=K zNA2SFpT>fV@YWCRfkzfU3)Ll6!n=nIXI0Wh$VyVU&$;U~N0~k7A)QOX^FYM#wylwg z&=na3u~cJm#xh4mqKD4Pf`=3Fi)VQnMR5aB=iJ%t(3&HNb7Kg4iam07&S>TknG9Gd z)4!wh2HT5Lz?{d&6r06pYm){X>@|E#W4_Zep$K$!TN`D#gnYzac_vd1%t~)FlF4q< z4xMvSP2Pb&r~85OAW^)ZJEWyNj)$;}y3GXijLetmQFToc6GP4;YlK>6vjANf96s|x z%PzaiRw9$-{6nbTabCm*7ayK(Azb1q+x$852=c`75J zu-X`C*BYg!Qj;p9O))pjYJf(TjSNf)7M~JeE&F(s0Fsg>?&&$U9&uE-%odl!4%3+1 z^*iVG3pMjh=2Qd*?_KKq-rsFBrk4jg`_J3%lqLje57z=zG&t(aMRm&KvQSa zE)>wGg(xX_bkQ?#@vQ4$dGlJBTt5L4kvNWLN@`Qd9YC;CKtVqs#%``kfN$ER7d)i0 zruOLvozbxnXRW|Q#ClfP7%I%32qF&iAS2qGD7~U?w565Wn?pSrRL;iOb0!~d z?T4)T3f3EJE-rQqZ_n680f1A=IYx^iY^`>!wYlCwCNP(BWDdqTVp1uI_Rqur`+=UD zCv72xb!GNBxhaaWUZ$&#N1>$7W@!%i3dk5MK=Vmt_hr1@>wY&kOi>i>q}zA8j-43X zj;Vm+XE*5)*QG1;er)v!=;-Z&CzkyiKH9brjv0Lf958qrV2OOkI>6p4%~(Lut%{?6w1EJ z{Ortho6MXNHYl7VSRa`AeCC4NNRW%?O*Mm+%mlm)Km0SVAVu{u+^?ftZiy9$1iXQ5 zy#O11uiM;}i8m1>$tLJCDH&0Wp#*pg@A-z#Ur+SW}_TT%m)>c#u z0G*LxB5Sfz1ktc7XW2L;21biaw^o<;pAROcV+~Bj9XlR@4}l3%Nszi^L+*gsxW`%} zkg`D3{zur>^K$J~OxLUd3Mr9RXK8An_wbNSS#?N`H0eA%W_Msdtkv*Nm&$8;D(Fk0w4rD}{t2TTx z$8eXM0PQ3B;Z}Bi0&?!fUioNbB`O8>0z3ul7OojXc}gdk^A6DUx&8Eq>~&OBP%`bX zJ0_hoXiPQbi0OqlH-7-{Y?%l9584xs8+|w&G2&n-iIwg`6L}~Lwk?L4>)wFZ*3E%c zEgPU80>-KRCW_#F#=K2#DjHQ)4G%!W+}xDGd%dh+S_pxnlqj@Adt?lPm0F0VTI~%` z3no#qWWLQfZDd62599x3_!BZzm_Yjq5$9hM!BA}#o*JJ8g!tZ{WmGCf2tP%b!z8M#A+x#kCv)Xni zu6_QarF-5nKf5U=32nZw!uMBs@@Z}Yo0a(ajI1q>Wo0wA1%LTO2cDVDH`beY&hacA zsE_Q3p2H7|j1D&j37a=VG9UJyn1dqD3b(OYU}Hh36#2!>Bt0sURK->h=MO){3QP`Y z0h7-2Bbh#M5{cWXR%GI#r~;cX2HJ~`xG^naj1d-rQB}jB2LTI(<=Bf z9~?XS2pC&CdKbC~Sdyt8n6c&!_|K}D@DT!)=AJefSl%Bd)Q%AZCCQU{TjzeHsWPe} z$$VNz^P=@SF@#=di;O~|PzrI@47`|=z`4V?VW&J-RPJ()Ux4IB+_@a4aqDV5M)F5cJo+NF7TrRI&dxqS!x`QyjnpDSL5FAUos4j+C195iGfNW@BZp$>(zaQhN?eZ$-E z*2ec?QNt$?jYNfk?7)itLaODI`C4||{mwf#@3*k>V&U~p$Cow5>iY=qgl<+3t+Cx8 zs--n4nrA{9kC+TCE%8(=JLzyh5t9T>EcpEln2S_UOI!%fF#@PS#@3_-pPS;7{M>qu zfu~Z|UJ8PDu`5G1mxHlq@5XEI;rQ&@^>4$^y;*Q8zD(kI%R$Qo4aWE}1_{U;cMUm*HbI{`(MT7xFuPcM#^I-}f2ZJbfbi zn_JqORU1BlANRs{S7XgXMqEW@b=rQI8SK9lrKhnX(W%R^nR|0X)LLh0q%-k%0}d(~ zkloZxZW1gn#v~gd)XQ}`16Xvs``VIf=vP_`9lf3Kuaz_4->YWAq`I-fQ;6K1C)JIC z@pWT&ArVVy>}-SuJC?!novYyet@B~Q_NCC+-3oOjH88Sz7(@(Vvb}%?mjIaDC&*Ee z?x}!Z!}oXL|0jz#61a8cEzl4@2s)YO?Zbf;eSUDOCk3~_lMr`2ZlKJR9MQ;#!wH2> zQiiwi{s(9C(wOqj9B5=B`Z2%2-%g^$fLUg2j36sM6D|J`zoDJ{EbYArv!62X8e*=L z%D_ilfpJDx_(W8gj#Le^NPx$@0`1&~3_x0MYJU_MaHN4gW zWBe6kfImGn-}ms}H}J!~`m+;&TpiLv@dg?;1kxmsQ=H4OBFS5O@U{mr@Zw%Hxz2oTed}iEO7%cxyc_{aE%Yz%r-2jDY=dL~ ztaU!Bq+#&#X!8q7WagQE{>S+JWdOxg`Xr&3l|WN;FHMq>uc(*`z=4B30nKq8X2Ai2 zr{st^ibNcackqdJz707w9Go*#w3iX-=d)annEDn+ike*@-@*0t9sToq{T%uS~hr{QVfO6Vzq%zrI!>Fk|s{s&poblN$;DB7Ue}$iaB@Bawq)3FDAC9jijTzw( zT+W(F`;;6HFZ1yH3bZ7id*mUKo^QO z0sDs@BnL9DW?)Ss_@8meBvf1hS_EGvdWwSynCs>_l!?0mkD#QNPrHH@73rbz93Qr5 zv0~B(uIcVpIspw;AmX*Fl=aB)nVrBj$B;};7H8?FGf?9hDNB{8^uC)6a5udVGuOR| zprr=J)sBLx{U*R(2wwIYIPEi?y}2jZ1Iu@=6cUqnH_wL^jjLfx$97yBQK&Dig&~!L zM8d#y|D`yWO6nN~K$QiovcTFY8aHzQzZ7+rX}M8~SQx~3BeX>(KwESSzHib)Ven!% z%Q3)A__vWv++j_S=Y&4EaVst53cHQlc`0R$QSHiBGSUP?{1?yxzWIJ8BTobbY`9%7 za@VMz;sU4PK_7cO$7#r*x8ejUHT#1zn~1!8Up!=ATaOgGD5P1YaV37gMq(FmINp=g zOatt8GT9tSX^IgqV3YogzrCJ=}hm<|T}g+pThL z1Qc9S4x{a|%ka%DIUUDy+rc@vVNw=?2bNv7cFuP|3z6?tD1#+3RQ{HEp>ZegAsb+1 zv(mDC{UrYTofIYDBBl)myds0^VOGYIn{Sd^BzoM-)_@F$DRqL;5c*W01fm>yD05(X zcG;j^+-sSUPKxJ;SHLmZBa@i$+`mY;4*?Y_FJk+Dh;6?`MjGlzwd05ByMYWDCLuc> zZV_!&$29l(IK&aO^e?T&S|`G5kxjg6=vv;Mjc2!k)?Y`Icb)jSglHlT0V6*&qC%>P2KfO+iGYUDl%b%46l8t{TiQbB z_Py8F?ONxYNAJ7uoW0lD=U#f>>-Qz=o_o)@&$;L9^_{iX@aGrrp-J5nv-wBtbx0QB zGMlGTSnt3o`_qh`868JTwhe5h-c7G&^3ukY^a9I^EH7<(g*NqX%|g1TO_-L6%qZwhH2oQs8 z{jDq?=Z$YSJOL6_a~^MZ+X#Y3P5wq@`8=vS>|6NvMMe%SLtV%5b}}q==J25vAAf*d z)1N-#eCR#Azo|4l@timTO9=TFDsQ-$188%B&Eb&TKTFH+X&Nn$VW&=?4S7r(KRNTykmZAXQ}z$`(Iab~q6dnRXx@nibsJCyR!WVWgRpW@hWO>n*r%M<2bu{S8{TeLcOlbuF#g z_By?~c@_O{%WL!sPgnE2js06_haY4THlC((TV`^5rH#Y;M4$Y>`Jjc@ZrO-gTYTdB z2FMutO`Y(pcTpd$?Os3wZZDd!bItCzrsNnH5jnBzPwWeisawYCo#u46mDLoAq<*)y zXG60y$B>gO&z=&GYxm+1`WVlboE>LXcG!(BEJGHSH)|G+u*Tezh50(_O}NRIW=_Dl zPsJf`>gUGVea?-vS`2q0Zfwsj{CPp*z4UrcC^9GBtA=M6F1KP-KH=qAkjp|5C4%-o zUjI7Ybl>6mWlG4}2u0{a)|PFFx7y$^QaXI!JFSns`^c@AQwKrJOdqgz$e|A1Iuz3i zVxl1rDUOuwY`ZI-0I6NmH`q_x2e)TYMUrrCyz8ck-4kiLkemrqX{PwY^vs|s-IHks z|DM8RVPf|rn%LEyS#Tuxi%3>b%MC>Z?0hriSN7S@L}UBFcG^0yg*NnWrcFHE+_#zD z=-)^i`Zi`{M2;^*SL0k)<~Tf=kI}>J5X;Fpa%$pd{qI88xvs-@D;&O!ZT_i~%%mLv z8S)W}O7FzQw5jVDny|ZT`|N@^I4-N`RF5TXd4UniXLyA}<*>}*tWYH~XZ$dNlPlCk z{OdODJrTf6@3Xow2MqI#$iI9sOY%l>Q*WXCFXoR+`PW&)l=84`n&X-qqhs8i{2@dC z*Y0Y(vk$rDyOcMmA5)&wWopngUEWdO&nG20VSW*H{M5A1F<~Kh8LultGy7DUX@kQu zZdX)~qigxsSE>1BY!?}v?z1Q+@$aFdNBA~1B{Qs4QoH8MeSea?KbD?e=zjKofzL7< z`?BZbbdn)5%isIa&LS#u9#8Q3%&nWG4mA{kZ^@-VJQr#)0=)HOUGLjy7S`uQE3lNv`|Oh&rLjn8Tx ziKS#hL3lKsd#nCetK)OZ`mH1>ifwLYxzKgUv)$$UvuK}{Jr`4-o5jDkc2d{#LB5cZ zf_%ss^5rZGttc<+pHV?n3^}Xnps0?90J{FTF|4GUn zP)pxhi#}=%A6vqgZzKt;cFm5=wTQ~l@uu^BF5qoG#PcND>Y&P@m@3lZvZQyHhb$Y; zxyGD8=kj@dm}Kb2Wr_T%$xzvKckW64E`(xJjb}p-F+ zAt@_Z-quP2Vcn)xX7@Ulp%jIuwdmA2r?}zSN80FG{=cRs_-h4Ojgb|Lb+3i@{0f(c zOAd6(v)$#lQNNo>y*-PWuuS6Lebix{kGzGS!}sR^ck)Zk*RFL$B}+BKAENlhFXb*) zLMy#iIqHatO!i`;aW1z_6p2($en!3Tkwm3YdC%f`NAc`~DX$?U3)XY{WvowftE_5F zCEo_3(rAS`iswt=zV7}mErGq_=a%vQB70|9Kt36X;?!Oz z?LN!);SB$u#nKVmhfBOpb^c4IXq%FxNNp6!yRf0t9_pR0J4=_Jz_l zASS}oEo4BW3_=RXqKK*#s|ygSD#ff(Qf2LIrxl{=ExwN8uJWsKfol{?#^#O3)$F6{ ze0{=wXt|%_5tHSlx1k-9b@SxHqB5e;{qB}Q>du^LCE**03IG6Ofv5-`E$RVn4t4Na zONAz(T&zH;3%uoq-r!^H($shHietD0g{@K4PGJeqIo8`qr6*loB+Y*rBVBc@EnZ<2 zs!JYSfan&)UP#Jwo%%569t~Z^u>4N+z5oD>38ErcMkQ&#TV+k~5d~YOAiPphImKJL z)e;dRE}p{4E4@+yyU1I98OaI}ychw{5Q-|xiYCK_#Zpmrez7RWnB~kV@2!}HFmUXb zT^`-=6uLA306>N%Xk(wE{;tI0xY zvC?RxIh?_=pdkn@@aA7dTr?Jr*c-i7@oR}zXy{9GOzn_O>iKx%el5AS#`O|8VDg0#9GAqx4cDR$@aeb*wQd zirE>@GbOw>cD}xi*Ic`O3K?=OY)5KnbqdKX@TAM1fnIcB004lfbS7Ssj+*6ht)kWV zam4&iS{0^Clok>l8w#izUI<0zSS<#cYZbaib%st-hlgV4*~%!LvU?@wV`Rkd#F9dJ z@iPDb01%Z9h9tI{@0#ic)N!=+3c?f1k78AnDWSJzZI87-6x|eaGEJ#vQ)Z3vqzhrL zn{dr1TsLR=Zjn}`bB~dz&^3vLfmaQA zIb&O#Vcl5bIxATUrS^|G7<=0_mU=yB@$z4zs{#N3L}hH?Hty7qEoz=5kq#??(bTU| zyfIh$Nu(-LD;Y_trK26nNo{i(p)0#yso}sH%ZG~)AnSP=2q$c5trBrX6?`UL(T5T$O&HUJYoMJsqG*gTWq(;%W2=S}qW8+Flhvqt^ zR_Lb0|E>hLQ(ggiCAug806zw1skcG)LcP z-s)rLVyRVRjiS+79b%+WQxnOW`=;4)lj4*ev;H(~e<{nk=$Zfk0LC^^8N$z5PGxz% zN(ot|sPj(8qEi%XL-O0GD84sQcDk)ecu< zAKKw0Pr9KDE0Jmb?IX)A*UmDG)I8i{8A7sGgoH59Vfl@QaA-(~#`03PdUF+t(S-HJlzBTV632+2t9dM^ zByujYT`V7s(mFDN(3G@U(%)at>mAM02harp004lflwlLgXIPG8xjXijpW@e`wG4#b z#I*#ZSy9Gn{j3}& z08xqJ8CF7PvvT=SNRe98(8qi!QV3DZ-=nInt*V=&&1aHI>a^k4WSAd|fbxNB=cskr zYzq>!m~uayGB%N3N@SWzpm=3FNlYfM#< zzu0j@)skar`l-T#P?kN{s>Gf0Ts6u*MiSc{jin_$6#fra`#gEuLPFRv0001pN-Iz( zTS*cpegsQG7LjJ98d9<@LK5mKTn*6)Int(BPTD~dLZQB7Y^Ni8sJPYwrr7UKSl&TM z-Ua{w0HV?+tYhW$X;w&|pgdt*P`MN7(%;7vp(d<3l{{FYgDAmrQDxyttzD#PpOpxO zu4l;lWeEAcm%lGy`8t2!jK>H7002a#-S{KrDfrK%+}9w9AjEQT_{egLl_143V5xT$ zI!0A!uEkfxVrkNlmy}_#vh$1#wcW;(lUUwMdDUY80000)LQK&nd zVlD8O8X?oH;9C)jt%+8OZRd>VQ+EA)3))fNRC%W)D@m=v&1gp{{7L79~>)eU>LL`2mswCG!ZAG}O?>ihSx+>OTq0000) zrPJ_3R(?nD^h#EkuY{DOCW%;?0*+Y`w5k15!fYMcQ^i~M9cq7bcyg-Wh=!yk5ULkg zzD&8ZHB#RL004lfbTqcIT;p@4CCaO}%t}$^kXkjYQI^u(8TX+j7G)vHnpFPU(gLZ_ zlCe5v3$&rs#d`l_x!mRC^k*zPaQg!Q0D!2B7W5U$>z%B~u4KiwTw@i`6uHx8$F*jF zt*T?A-M)(8#7IS1iu(}Zh$Xp83CC6vlcBH6B>Ib|C$Rhqw>kg-0Eo(HCoI>nED+Md zTJ8rF85XKinwrFBA*)8LxT4i>Lz{!eYTdBpf3Xf>#jbCln4t{sT*Y!Y<=Kjt;MN8J z002=LYj~FB+pPQ!_xZ5UlNRM#7Sik#B%w<2ha7x0L?@NV)FBc?@lax|4~o-WpTF_s zBFcw%u4Z`&w=n{uOnILnu_(N9M+hsGp<0PfYTAEI`(k-#e0YDa7KG%lEVr}#+fiGR zzX1RM03a%R1Rg8odn^-KPGMQdiuG((u&46WRb^?dA~a=+-IB~Lg@9=G8}dU*4a4@i zKadQk-9`B@o20Zyz7qfd01y?R4Ukah`wICkE8#;5(jvtXC1m!l{IsgCwAS%FH56OCaLm^ZUm6KUHpU%^FvCL%| zPfBZq)-vJ)nJn)8*UT45;91T2+_A7)ubxy9sJ+!_D?03a$r=i%u>mK4hWfP$0)5l7f3c-}hrm;e9(fT#eY5335f zyMYwU;K>|T_J{f00yA4`)fB}P_hp%!av-Z!B;Yi=g=Hl#@8xCBQ=VW=$jOTZN!gCS z6#xJL08tscSkLlMASH!qvT^Y-EI(+eUqjdH8Z1j-$JyLow6 z({TnVcVz8n*~YS!_p{mOLp2+G9*(`5~M5okS%IT_GWmg=9jK$u?UuTle%*=l)Trdun>RXSx$W|H%1# z0^QTKoT~FX&+>bo=N!QY*y=-mg1*3j?|=X#PzKBa&HzpYW&jg`Q9!8xmjnyw1v-Ge zz;<905C+x&o$Asu^FGe+$Jz(e#D@e5O?{jRECdz<9|wwabwUpRHwml-mIDpyvjteG zdmjVIzY#c`gQ28^4pb;Gd;>UZC;$xg_dMEG-~r%qpdCmY1IfPuI1~_^z|epRz~_LQ zfH5Cb=kfql6bJzK0!=|*Ad%{$0G1vD$wvo{3~3>N(ZHvHTY*XWNQ2bxM5$)j%zOC&z>(6?YNjm)9t5U39F3zUOQe(}rA5L@ z(wB@8O~&X;#)zf=A17%gNlFVT<tl^aY+f8aFp{V2p-vHG&g>aY#xK#1$Bu%v$fB z^vo1s)_O4-!=sSQ14aPW0P?55^#HqpO=hh(;T*&au$`dPXEoq<=+q>FEoQBEqtOr^ zgWw9~U53nBZ_^;KHHM3UbAi)=H$|oay0jk{qk%sF^?(#0WTBuy(wB%)=_zM&#c|9W zIh~WLr*qn=v) zjInb_$|O?Cto@VO@$xa?W;FqhR;^rx;J<*c1G6%e^?2Y);9&&cHX6dCZD2@%69Ek| zjNp@afnNaMG8)6BfDMcl;VHn6f%}0)1AsBm!XJa15@5PhL7?+(P*N;yYls>p$pDUX zWvUZJiJuGHo;mYm9@0WeNn9qFTse_*C!WcIN#`=EWW2=H+ zb@GSqZjC6e=1a4#B$kZvdh=>ZTqSt{S+$$6L;vdvRV&!(2ftLnkiaS=KSQ$8TIean@0qpUyK(_z zn1P!G++j3^v#M7Y0~yfq*g>m;sA@|}+?tyUCw-i2PhLWqyRGSWd$qL)}QMnYN$ z3A(0Hq!&@)E~BQndf01BubxER?i7yC+4 z;7POA`+k<|^LyfiOhboO=e zPWv{R4)11vZwpdQG?H=A;oP zP*GHFOAjIPeTGWl>iUp>L(ms!E5P=cp)Q<2@M7jols936MT!Fmxl!42Rlspbl4(fT z1BTHUJ_SUPEmfqY4BRiNb5M50L`WM~62z@Y*>8Z#v<-4Ha9xHfNlC2LNY*3xjRM|v zMniZ5l1)ySx*SrYTIt||;un3%7*i{c=Tp-cP~)i@@@*dMZRPbnt9hq=8_nJOY3XgF zHyK42I-0PbZlDotlV+AoQthe2Y}v$;6Bcsn$dj^OzuHs5w5myL?%0l7xC)UT*8mRz zZ3Rk?)PJoMAhIG-3{WFtkgSkQH5$XWBk>`mRA*X>kshaxdI!M@B!=TPi5&iz3#s^y z?5ub%NghP!3PKU}25cD~HJ zZST>Oh~Uz6bfHnI49MZa{gFhJ)%(}e73=25XWxoj8$9^MdNHF)YOva^LbY!Ru&6%d z-xTx(y7QDCJAjMSKwJnc8^nATrQg&@V6OUd7{NXyy2CR$8JL1#b_SqQZ98zYS?k?t zG=wYDQpc7Ew*j#XS|FdM;WKu`5SvuQLWcFt!(EuM@s zxpE?Bjhj2b6 zbIK2DpUb9Fhv?Q)^~sIka^r>YB&WRrU|!(GXbhJD|KU&#JIq?|yNL9}6u?_Z9tSoe zcn85oU?cDmv*h<$*s@P7VSXS^Z~!w zLAM04WE@K-(KQ{n=0+Dfu8d>T01z67`#N(5@)A}uZ@^_d*EC?(Wg&l4D#dV^whGvS z;3Qx&7JoDv!grZ<-WO8p#b^vq2EGSetM1ec1P25??b&~j1OZ$;ul}CuVieeG)_Hpd z#8xDKKyWwULU5DO5Z2K6g}K1nHNe|RskuyPr2cQ7>BU$cUWU%kfO|6Mj!T&`mkjgb z1_8s87URlBbIQmWIp5xUhjy}j=PR^E+Hqy5fv#8&Qz|DiYt%_ht(=6%s0njel}Ca^PVT{*?Kq+60F;341XQT zas(~}p8y^-8pB(F-AJmC%vAA~VvrV+rHP4&r2~3ocU3nRd;5U5hvHV!rbd!`ad8oL z*+&h6Td=sqXb5jX(Bt$KkOa}R*GQW(j%zLpV=nT>jK=UI%B{bCfVUF#1!R55zc61r zBdt=bIAcpkF>}NTIge@B`5N0gb`FpNQc7Z0oa<&?$^7wNsy$Vdx{J|-M$$?WN%j$m zM>y2i$v^h4W5v$bI2`Y$)K!8mG>$7D$At26S?|{qkFdAf;B$3We>Up!Ww)E&T8pAgrxgWtN zfpP?=gYbe(4loJ@7YK{_5~(>%VVBe5G;6)BMnm{_2u=phNo(o)kiSC38iv%#+5R@F zWCSCMYjR4B?T6pT!g8>=HhORJUdLtA>zG`5995o*v??jmi>NB9U|iW4P9AXrSDxhK zXP@{U*PL`2iDVp0T3kM3QEnGG*3I7T1L%%$z9@zuYT=x8hXrhm8hnm!689E*` zlwAePUJ~-Zz0?}E^N6z4!iDyvkDlW{=bQLqBdJ4yv zk9WBE16ymPowfVl!lM@r`wYXR70y;hV|+L(%*(jK=jZB7`&-&XM^h4P{d?N(wa_jbW2ia9XpO60^=5@4uhk@n~z6$rG!5lKWjcjB2j&&j6^ zC2g8IceAN&YgUF<6aak1Ie79Nq)bl4teb0P#7zL0()R#ULVkj^PJqm{06zKbcX=)Y z7}>a%%&ZK_@+1ZB4KmOsX8i@tzaYb1cgr=I;fyEQH29SJUZ_Ew%> z=(SX27nd9u=g&(7C#6M+tC%sR*$29%x1FANZ`yUW>2Nr)W*Q#7=mQVDwM9Bu)x2hq zM?_(uNVRcJHJhuY2G5Z-H&q4I&Q*MYQ2P6yz%!}Dq9q|eOKa^xwScF60iL_`r>Sey z0MVc?5Xn{ds~GJm8ti$>7f3Ai1y~XSb8!D^u#3zOkBF~I{jCJ3310fUy-PxUgq@&^ z-k-X*M^W^xF7=@x5p3six~a&Kw9^&uCX$S%Q=F30Vr=QCA>%HOZ+@C-Rg?JG_`-85 zcXc(hvUyFx>MBPYF`W01kxE$({7S&J(w+k@*`K(58gMaiV|~c~+n_ISXR5%;k#Tnl zaEl6nRROX-GASl+0a_awRqK|ko(H~Thk`cu< zIoV4oCCi##rZ*8~Ovy+VO*x;%Q!l`shm<+k+sYGLpGDVPACw@f84etdc+?o+HQ=Na zW1%L~0TtmKt)$Q>;EO7__l)EGFJqUJW`@j90e%jAwm#(lQqULJ==g21@~B2i_@9_9 zsiGO@FTh^&M3*)fvQe22V}PiH3xQAuFnqvolsk@uIU07xQUQ@B5JC)MI|oK-W5lgE zDK1JP)5*hG2RPD7%!(tC9E`N_Uz?ub!HtjS)hCzcqOzzQD-UKg&u1(Z!_@OQxQPdp zjgpFgTqB@L+Nlr6EAZT=exIyVPO4q2T*nkJ;y{z4vQw}6bl@SSvYdf4>G=Jw`jB7G zD7VlZ!B`hgy3N7Ps&lI9L;fEDPtjjtuphWd%ZGr;rebmpwC$N#SnB9hBspm%bJm^| zX|{)vv@FJ!j^>_oZ{uroznGs6D6Zz3lk1sKKDIv-?kJkHXWOuqfC7#KmNA{ zGlg9$Iwj}}90D4#S1Qs|woHAgtVDJ> zUR=eA)l)fZoWZ5jE*_MNFHR3Kvg0mzQ>}!lPW?LaY8||4Ygz@Rs6-62L2pAMw+l zi=hK`>R(E_G%5g0cbwZ0^aXY~T~zh?srr!LufQ?g5!H3J{VI}Np@^;dX_hv(8}E`f zu&;M$jV|Espf7N@(@53m&ri%MhO(s>SQ7fI*pQ@^ttwPj7lNL6gyx?8SkgKQk#ASR z7S zP#^M_rv5Dn`BQG)At%j~B_!~5$N0@w2Bn^D5;)FrD_cmXUrJU8Fnl5Z01rTOaQs86 zip&?Nplo zIiJ2{UtY1H9IZW1aFjS+P0VqQ0LmFyN1_39>O=nHl;J5IZu1@WA^%F?Md!czkpFey zqG4h~o3XR^de!&q6-AE2Ak8%)_5#l;wTFa7I(*CNjcEe57sw#u(scTgv77@vp?nOL zo{9mg$E~?oec&y&b~Gu-90?X-`SkkeY#^kY;NDqo&WqbzgzzpZrx2!yq9~; zM{}Mtv3wk7jW>uT<3mcpApo*DMIR5--c|3-g}sdC+t}4-T}t}A2mD0B1Y?)|v~6eFfHrff7x*`H*s3!V24@8nPKE@f9&b5{EyZpAs&*U6TS z?cBHiQEqwtPPQH1NqJFenlO96tC^Q}{XM7cF?-a>!%C-Y08!sJ$PA+{$LXmY%_88& zK1c?Afm8|URlpfRU*PVbFYq3Att-F;Vdrbc9V+aH0?rcl!Z_!tgroCx+ckbMpYc$J z>}Qp#$SN`Az{f0d7@h}#`_!ZWJD2#z6zWZBch)_WB|m`u5rWRXE_QY0mH;zH%wS^K zSoZZCKsq>)a#tBEo7d7BZRgUd3z%L#nIg@Ng++I)mmP=S=Z$?~*6d$TkzO>YNLmO% zG}*`VO)qiLq_cCVwXvK~J(>4A_YO)o94efzzHfMXh;Y3_C%*!0!!A)+$u+^SQ=8c`rmP0$}iAT~2I1)*j zyM(60ySaPqeU!S2sq|D3O+-1=*M%lDJbDqOL&RaEl(a=UFb{6R7<1~N-$#{iXRR1c zhRQ5<#lD_{yxp3Ucj4km=P-NJ$yu+U3!TcMa&%3nJJv(evZyR7r`%mSpzJ_onJPjE z;xfUuLp!pQYMx3al#Nl8eEwkp;3ZW4=up5=FH{D7f#ukX!EBfPg+!@%$xpG1(LV)v zgdJ~r5_@^>{Xnm^h)To)QffW_YUA$)t?^h;%=m^!lbe} zb~~lul8~RLe1TT%B{x>5oWB%L|9qjYr6x35dfV98x;dwQFLrrYJoO@`R34x99z>QP zo+dPe5JRT)W!eZ*N}`Fr9DUO%FDlF1|8gAlT8ekbGw?>x7kCIeRR0imcz&y*2lfI@ z(vGG-C2ddThe2OJDv(?!;kKYJuyTl7{Th3*yDOCn1Ni^gGyGQIeP>@3Xf5T^{$AM< ze^ckJ1%8CR{>Fc2dw!Kxa0l=haH~)k6~LRQ@`T}-#Qqpcf(hm0@SpZs=8c)1^SD*b zYj|SIv+U~H3qlOY@ywIb9`HStPjbU)S98JfAJ2N;vZj~$W%wZ~^Ir>O2T&XI1-9o~ z5}9Q~fN~XMNtGLIEV_b%yv`Ugr+i&m4UpbW92y8uoe>j}$ssaVx>X zDd%y+Y1d?}LQA!E2R5**>1DQd?4YC1E+5cT8Dut@k$E49OfN8}A^6w?FE<(2QIaEv z!_94>>GFk*!9`pse zhqE`*0}CTSoS3l~Rt(r_)_U8G7s75NlaY)<2n%GY3Oh`c45KkT35XzQH5Yg#fU)AuQ3!E7P-NHKi-TsZu~YSVjQyK* z0ZA-c(CAU#mX)>*1j0rw9WF-((IKVdepCo4rVE{ey=^ROdYP$}$1}C+_?*X%D6VGl z)C*ZW^+Mk7Y-U$iGY5KF=!hMrE7qeb-}-G6A?)(TQdbG(?o!56Jys`z=Z%JN zz^wJ|G8)68mwLgy8uG5kl6@0hh->74!jKoNH$Wh;`~fIg!!ycon4AidZ% zG#1D(aFx*zz8oQzfSjZHn?Mt*L9SD_OE1uez_NdrZeWLS>Kvsj-z5K<(E;jZ{&)lb ztg9JadSpq&v1FXM6-P=7O=!3@7exi;oJu6m?Rbg6#>de$Js*|T30xNR1zyQpB{YVG zViqsKUfZKrUA6{0E$|s6cLHk>v}11`RHSC;Gz4>z{7|jh^8@Uhj9u8q>jdn@b5;R< zv(~$L@cmZ_DCA>R;0W0Duf=Yz0b!K z*3RxWUJScQQiWh0kVMMQBed6bM2|$uH*4qis{TBMofhy@;AAAZEMF49^V|~{lk0GIUU2D0{kWD3mk?5 zHtrDv1XYL75^;~aSVUvd7&gBk)DjC8P;E4ZKdp$bUJx^oGKQc9!D&c}kUU`4c~_}x z9yS`oKUJjUVg=rAa1=&+il8qBf+Wb(RWbJgyBH~PhS3T$sLbM%Wh`x}P zlu2IR^>_ZT;ZMBP@@`IGi0nMV`i$q#lNpf!I)YD2$rI7O=%1Ch_bFhFnz1^PO6+vP zn}OREXR#H@GN2g2LF~NI{-vx-*{1?>6-X{H8pC%Rjp1~~x_SOA=Rz%+nTf3*R!c@D+hX7hLji?>g2zHzZJU7WR^Bo&GHl@`3Uxo zQBf7II;iZ5zuBIP{0{`vk(4Ss<2bc&+v{m#vxhSgOas;dry^|@HbsOPFN7c6twZIdK4!_N443{D?fw2g_j9nP@Jp|3bmq6TXG=w7vc4Bcgf)T)9NXm`q z+H**Gsagi&dC@oOIkz|eY8hAXoX42eSZ3D88Y1pfO1kf>vOPK&+R@|3vZ&1V2NtSIu@!t!cW)2%sXQ(s*32Zj&yzdzeVLyWT*z1xNtBCSC$J4X`4d}cyz22eV%w;%gM&=-gwjh$gM z{2kB*6qT5D-rj7w+juc7uyA34xzIZxKg8Q$7D*|ExzO9^(En~b#4q~HTBA3cU0^hX ztAHY1)7#fAn3HrGnU1BVs?zIxOS;X4-k9TBRoEVW5{q7Qp*NPU{$CMx11@u+H)1rt ziCH^u=s`}sz{^5@A#AtmUUF>6Q>ak>ur>tDaqTt*jKI+XBcJ$3mR%Jv>%4dyO~SQC zCJmn>e$vm1=GvdddA*!*m?4&SaC$Fka4la4x{g_E3=fW!Epn+{1+{Q-g_Q^lyyS@)GKWKcgS z01a`SM-_;72aoM5`{9Bk3qS_!NPQ#&p3i=>N3bXDu^p-Z72wEx(Y}yhxL~x**vtMS zQW=@;*k1PkYH(z;Lzaa6nzTdBp95|ZFh+g=QX|h6#m=d`7ibFl0*P#WJk~xUaHLws zhJh&NtpeNa{RRa_b_;AS+5i9m diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index d1531649e..46b64359c 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -71,7 +71,7 @@ Berlin 13359, Germany

-
From c7510890c82dc1972ddd3279cd1e82271d24cfc6 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 19 Oct 2015 08:46:15 +0200 Subject: [PATCH 251/702] Added configurable api doc link with WebUI.apiDocumentationLink and using it on index and default --- src/main/resources/props/sample.props.template | 8 ++++++-- src/main/scala/code/snippet/WebUI.scala | 4 ++++ src/main/webapp/index.html | 4 ++-- src/main/webapp/templates-hidden/default.html | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index c06e88a4a..4b6b531d2 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -105,8 +105,12 @@ webui_index_page_about_section_text =

\

-# API Explorer url. For now, this is a page on Sofi e.g. sofi.openbankproject.com/api-explorer -webui_api_explorer_url = http://localhost:8081/api-explorer +# API Explorer url. Change this to the instance of Sofi linked to this API. e.g. sofi.openbankproject.com/api-explorer +webui_api_explorer_url = http://sofi.openbankproject.com/api-explorer + +# Starting page of documentation. Change this if you have a specific landing page. +webui_api_documentation_url = https://github.com/OpenBankProject/OBP-API/wiki + ## For partner logos and links webui_main_partners=[\ diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index 4ffa0affb..6723c59f0 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -61,6 +61,10 @@ class WebUI extends Loggable{ ".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", "")) } + def apiDocumentationLink: CssSel = { + ".api-documentation-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_documentation_url", "https://github.com/OpenBankProject/OBP-API/wiki")) + } + // Used to represent partners or sponsors of this API instance case class Partner( logoUrl: String, diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index f99851887..ff4e6eebe 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -38,7 +38,7 @@ Berlin 13359, Germany

Get Started

@@ -47,7 +47,7 @@ Berlin 13359, Germany
  1. Register / Login as a developer here
  2. Get one or more developer keys here
  3. -
  4. Read the documentation here
  5. +
  6. Read the documentation here
  7. Explore the API here

diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index 71bba7d1d..fd2f37c08 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -115,8 +115,8 @@ Berlin 13359, Germany
From 9a8487d49ebf4ffba47132b9c6d58c3d2b6b7e5b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 21 Oct 2015 18:15:48 +0200 Subject: [PATCH 252/702] css style not used --- src/main/webapp/media/css/website.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 5a6535ae6..f19bfa648 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -94,11 +94,6 @@ input.submit { display: none; } -#accountShortDiscription { - font-size: 20px; - display: inline-block; -} - #social { float: right; } From a575f301d9299b0ae75d54a0d5c8fd0b6528aa4f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 21 Oct 2015 18:19:08 +0200 Subject: [PATCH 253/702] Adding support for different main and override style sheets. Adding override css folder --- .../resources/props/sample.props.template | 9 +++++++-- src/main/scala/code/snippet/WebUI.scala | 19 +++++++++++++++---- src/main/webapp/media/css/overrides/bnpp.css | 9 +++++++++ src/main/webapp/media/css/overrides/rbs.css | 0 .../webapp/media/css/overrides/ulster.css | 0 src/main/webapp/templates-hidden/default.html | 10 ++++++++-- 6 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 src/main/webapp/media/css/overrides/bnpp.css create mode 100644 src/main/webapp/media/css/overrides/rbs.css create mode 100644 src/main/webapp/media/css/overrides/ulster.css diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 4b6b531d2..d60d113ec 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -96,7 +96,6 @@ BankMockKey=change_me ### web interface configuration -webui_header_theme_color = #53C4EF webui_header_logo_left_url = /media/images/logo.png webui_header_logo_right_url = webui_index_page_about_section_background_image_url = /media/images/about-background.jpg @@ -115,4 +114,10 @@ webui_api_documentation_url = https://github.com/OpenBankProject/OBP-API/wiki ## For partner logos and links webui_main_partners=[\ {"logoUrl":"http://www.example.com/images/logo.png", "homePageUrl":"http://www.example.com", "altText":"Example 1"},\ -{"logoUrl":"http://www.example.com/images/logo.png", "homePageUrl":"http://www.example.com", "altText":"Example 2"}] \ No newline at end of file +{"logoUrl":"http://www.example.com/images/logo.png", "homePageUrl":"http://www.example.com", "altText":"Example 2"}] + +# Main style sheet. Add your own if need be. +webui_main_style_sheet = /media/css/website.css + +# Override certain elements (with important styles) +webui_override_style_sheet = \ No newline at end of file diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index 6723c59f0..c21fec415 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -32,13 +32,15 @@ Berlin 13359, Germany package code.snippet -import code.api.v1_4_0.JSONFactory1_4_0.AddCustomerMessageJson import net.liftweb.common.Loggable -import net.liftweb.json.Extraction -import net.liftweb.util.Helpers._ + import net.liftweb.util.{CssSel, Props} -import scala.xml.NodeSeq +import net.liftweb.util._ +import Helpers._ + + + class WebUI extends Loggable{ def headerLogoLeft = { @@ -65,6 +67,15 @@ class WebUI extends Loggable{ ".api-documentation-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_documentation_url", "https://github.com/OpenBankProject/OBP-API/wiki")) } + + def mainStyleSheet: CssSel = { + "#main_style_sheet [href]" #> scala.xml.Unparsed(Props.get("webui_main_style_sheet", "/media/css/website.css")) + } + + def overrideStyleSheet: CssSel = { + "#override_style_sheet [href]" #> scala.xml.Unparsed(Props.get("webui_override_style_sheet", "")) + } + // Used to represent partners or sponsors of this API instance case class Partner( logoUrl: String, diff --git a/src/main/webapp/media/css/overrides/bnpp.css b/src/main/webapp/media/css/overrides/bnpp.css new file mode 100644 index 000000000..c79588dad --- /dev/null +++ b/src/main/webapp/media/css/overrides/bnpp.css @@ -0,0 +1,9 @@ +@import url('https://static.openbankproject.com/css/bnpp/bnpp-fonts.css'); + +body { + font-family: "BNPPRounded", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif !important; +} + +#header-decoration { + background-color: #499A5F !important; +} \ No newline at end of file diff --git a/src/main/webapp/media/css/overrides/rbs.css b/src/main/webapp/media/css/overrides/rbs.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/webapp/media/css/overrides/ulster.css b/src/main/webapp/media/css/overrides/ulster.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index fd2f37c08..d6492611f 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -29,23 +29,29 @@ Berlin 13359, Germany Ayoub Benali: ayoub AT tesobe DOT com --> + Open Bank Project: %*% - + + + + + - +

Get Started

From 9ea88c2ce0eb06aef7de28e8a54e52b2bead2cb2 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 5 Nov 2015 12:56:20 +0100 Subject: [PATCH 258/702] Updating documentation on the Resource Docs call --- .../scala/code/api/v1_4_0/APIMethods140.scala | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index be2a9ebae..7b674fe19 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -302,13 +302,24 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/resource-docs/obp", "Get Resource Documentation in OBP format.", - "Returns documentation about the resources on this server including example body for POST or PUT requests.", + """Returns documentation about the RESTful resources on this server including example body for POST or PUT requests. + | Thus the OBP API Explorer (and other apps) can display and work with the API documentation. + | In the future this information will be used to create Swagger and RAML files. + |
    + |
  • operation_id is concatenation of version and function and should be unque (the aim of this is to allow links to code)
  • + |
  • version references the version that the API call is defined in.
  • + |
  • function is the (scala) function.
  • + |
  • request_url is empty for the root call, else the path.
  • + |
  • summary is a short description inline with the swagger terminology.
  • + |
  • description can contain html markup.
  • + |
+ """, emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil ) - // Provides resource documents so that live docs (currently on Sofi) can display API documentation + // Provides resource documents so that API Explorer (or other apps) can display API documentation lazy val getResourceDocs : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "obp" :: Nil JsonGet _ => { user => { From 23b009ba3f003bfa802b1927f842ae75a1c5fe50 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Sat, 7 Nov 2015 15:34:10 +0100 Subject: [PATCH 259/702] Added hint about encrypted Ubuntu home directories to README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index be99f8906..f17eecf22 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,18 @@ To compile and run jetty, install Maven 3 and execute: ---- +## Ubuntu + +If you use Ubuntu (or a derivate) and encrypted home directories (e.g. you have ~/.Private), you might run into the following error when the project is built: + + uncaught exception during compilation: java.io.IOException + [ERROR] File name too long + [ERROR] two errors found + [DEBUG] Compilation failed (CompilerInterface) + +The current workaround is to move the project directory onto a different partition, e.g. under /opt/ . + + # Databases: The default database for testing etc is H2. PostgreSQL is used for the sandboxes (user accounts, metadata, transaction cache). From dbf7dd308c4c8dd39ebfb8b91c173a894c823506 Mon Sep 17 00:00:00 2001 From: Stefan Bethge Date: Sat, 7 Nov 2015 18:50:41 +0100 Subject: [PATCH 260/702] transaction requests fixes --- .../scala/code/bankconnectors/Connector.scala | 2 +- .../api/v1_4_0/TransactionRequestsTest.scala | 61 +++++++++++-------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 418caeca2..07af4cdbb 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -132,7 +132,7 @@ trait Connector { //set initial status //for sandbox / testing: depending on amount, we ask for challenge or not val status = - if (transactionRequestType.value == "SANDBOX" && body.value.currency == "EUR" && BigDecimal(body.value.amount) < 100) { + if (transactionRequestType.value == TransactionRequests.CHALLENGE_SANDBOX_TAN && BigDecimal(body.value.amount) < 100) { TransactionRequests.STATUS_COMPLETED } else { TransactionRequests.STATUS_INITIATED diff --git a/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala index 55ed4be19..531fdecbd 100644 --- a/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala @@ -74,12 +74,12 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers //call createTransactionRequest var request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / - "owner" / "transaction-request-types" / "SANDBOX" / "transaction-requests").POST <@(user1) + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@(user1) var response = makePostRequest(request, write(transactionRequestBody)) Then("we should get a 201 created code") response.code should equal(201) - //created a transaction request, check some return values. As type is SANDBOX, we expect no challenge + //created a transaction request, check some return values. As type is SANDBOX_TAN, we expect no challenge val transId: String = (response.body \ "id" \ "value") match { case JString(i) => i case _ => "" @@ -141,36 +141,24 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers } description should not equal ("") - //TODO: check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least) + //check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least) //(do it here even though the payments test does test makePayment already) -/* val fromAccountTransAmt = transJson.details.value.amount - //the from account transaction should have a negative value - //since money left the account - And("the json we receive back should have a transaction amount equal to the amount specified to pay") - fromAccountTransAmt should equal((-amt).toString) + val fromAccountBalance = getFromAccount.balance + And("the from account should have a balance smaller by the amount specified to pay") + fromAccountBalance should equal((beforeFromBalance - amt)) - val expectedNewFromBalance = beforeFromBalance - amt - And("the account sending the payment should have a new_balance amount equal to the previous balance minus the amount paid") - transJson.details.new_balance.amount should equal(expectedNewFromBalance.toString) - getFromAccount.balance should equal(expectedNewFromBalance) - val toAccountTransactionsReq = getTransactions(toAccount.bankId.value, toAccount.accountId.value, view, user1) - toAccountTransactionsReq.code should equal(200) - val toAccountTransactions = toAccountTransactionsReq.body.extract[TransactionsJSON] - val newestToAccountTransaction = toAccountTransactions.transactions(0) - - //here amt should be positive (unlike in the transaction in the "from" account") + /* And("the newest transaction for the account receiving the payment should have the proper amount") newestToAccountTransaction.details.value.amount should equal(amt.toString) + */ - And("the account receiving the payment should have the proper balance") - val expectedNewToBalance = beforeToBalance + amt - newestToAccountTransaction.details.new_balance.amount should equal(expectedNewToBalance.toString) - getToAccount.balance should equal(expectedNewToBalance) + And("the account receiving the payment should have a new balance plus the amount paid") + val toAccountBalance = getToAccount.balance + toAccountBalance should equal(beforeToBalance + amt) And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) - */ } } @@ -216,12 +204,12 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers //call createTransactionRequest API method var request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / - "owner" / "transaction-request-types" / "SANDBOX" / "transaction-requests").POST <@ (user1) + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@ (user1) var response = makePostRequest(request, write(transactionRequestBody)) Then("we should get a 201 created code") response.code should equal(201) - //ok, created a transaction request, check some return values. As type is SANDBOX but over 100€, we expect a challenge + //ok, created a transaction request, check some return values. As type is SANDBOX_TAN but over 100€, we expect a challenge val transId: String = (response.body \ "id" \ "value") match { case JString(i) => i case _ => "" @@ -272,7 +260,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers //call answerTransactionRequestChallenge, give a false answer var answerJson = ChallengeAnswerJSON(id = challenge_id, answer = "hello") //wrong answer, not a number request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / - "owner" / "transaction-request-types" / "sandbox" / "transaction-requests" / transId / "challenge").POST <@ (user1) + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests" / transId / "challenge").POST <@ (user1) response = makePostRequest(request, write(answerJson)) Then("we should get a 400 bad request code") response.code should equal(400) @@ -282,7 +270,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers //call answerTransactionRequestChallenge again, give a good answer answerJson = ChallengeAnswerJSON(id = challenge_id, answer = "12345") //wrong answer, not a number request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / - "owner" / "transaction-request-types" / "sandbox" / "transaction-requests" / transId / "challenge").POST <@ (user1) + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests" / transId / "challenge").POST <@ (user1) response = makePostRequest(request, write(answerJson)) Then("we should get a 202 accepted code") response.code should equal(202) @@ -318,6 +306,25 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers challenge = (response.body \ "challenge").children challenge.size should not equal(0) + + //check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least) + //(do it here even though the payments test does test makePayment already) + + val fromAccountBalance = getFromAccount.balance + And("the from account should have a balance smaller by the amount specified to pay") + fromAccountBalance should equal((beforeFromBalance - amt)) + + /* + And("the newest transaction for the account receiving the payment should have the proper amount") + newestToAccountTransaction.details.value.amount should equal(amt.toString) + */ + + And("the account receiving the payment should have a new balance plus the amount paid") + val toAccountBalance = getToAccount.balance + toAccountBalance should equal(beforeToBalance + amt) + + And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") + transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) } } From 91491b3ce9d8c8406cc83d8e83c85795a0191486 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 8 Nov 2015 08:50:20 +0000 Subject: [PATCH 261/702] Comment on ResourceDocs description --- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 7b674fe19..360ee7203 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -320,6 +320,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ ) // Provides resource documents so that API Explorer (or other apps) can display API documentation + // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple various of markdown. lazy val getResourceDocs : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "obp" :: Nil JsonGet _ => { user => { From 02a1c1b0b00cc7097a260f11b762a0345ff09de3 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Sun, 8 Nov 2015 11:55:53 +0100 Subject: [PATCH 262/702] Added example import data for the sandbox --- .../sandbox/example_data/example_import.json | 12424 ++++++++++++++++ 1 file changed, 12424 insertions(+) create mode 100644 src/main/scala/code/api/sandbox/example_data/example_import.json diff --git a/src/main/scala/code/api/sandbox/example_data/example_import.json b/src/main/scala/code/api/sandbox/example_data/example_import.json new file mode 100644 index 000000000..e12926ec4 --- /dev/null +++ b/src/main/scala/code/api/sandbox/example_data/example_import.json @@ -0,0 +1,12424 @@ + { + "banks":[{ + "id":"obp-bank-x-gh", + "short_name":"Bank X", + "full_name":"The Bank of X", + "logo":"https://static.openbankproject.com/images/sandbox/bank_x.png", + "website":"https://www.example.com" + },{ + "id":"obp-bank-y-gh", + "short_name":"Bank Y", + "full_name":"The Bank of Y", + "logo":"https://static.openbankproject.com/images/sandbox/bank_y.png", + "website":"https://www.example.com" + }], + "users":[{ + "email":"robert.x.0.gh@example.com", + "password":"3e3a3102", + "display_name":"Robert X.0.GH" + },{ + "email":"susan.x.0.gh@example.com", + "password":"58da7854", + "display_name":"Susan X.0.GH" + },{ + "email":"anil.x.0.gh@example.com", + "password":"90a66977", + "display_name":"Anil X.0.GH" + },{ + "email":"ellie.x.0.gh@example.com", + "password":"a2953da3", + "display_name":"Ellie X.0.GH" + },{ + "email":"robert.y.9.gh@example.com", + "password":"c9a641df", + "display_name":"Robert Y.9.GH" + },{ + "email":"susan.y.9.gh@example.com", + "password":"b7c78269", + "display_name":"Susan Y.9.GH" + },{ + "email":"anil.y.9.gh@example.com", + "password":"fec9556f", + "display_name":"Anil Y.9.GH" + },{ + "email":"ellie.y.9.gh@example.com", + "password":"6f821300", + "display_name":"Ellie Y.9.GH" + }], + "accounts":[{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh", + "label":"Susan X.0.GH Current Plus ..898", + "number":"18449011898", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"6599.63" + }, + "IBAN":"BA12 1234 5123 4518 4490 1189 877", + "owners":["susan.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh", + "label":"Robert X.0.GH Current Plus ..251", + "number":"11875446251", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"6379.63" + }, + "IBAN":"BA12 1234 5123 4511 8754 4625 177", + "owners":["robert.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh", + "label":"Anil X.0.GH Current Plus ..996", + "number":"10433713996", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"7588.25" + }, + "IBAN":"BA12 1234 5123 4510 4337 1399 677", + "owners":["anil.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh", + "label":"Robert X.0.GH Current Plus ..849", + "number":"14444021849", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"6662.05" + }, + "IBAN":"BA12 1234 5123 4514 4440 2184 977", + "owners":["robert.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh", + "label":"Ellie X.0.GH Business Current ..272", + "number":"18953434272", + "type":"BUSINESS CURRENT", + "balance":{ + "currency":"GBP", + "amount":"3748.57" + }, + "IBAN":"BA12 1234 5123 4518 9534 3427 277", + "owners":["ellie.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh", + "label":"Anil X.0.GH Business Current ..015", + "number":"12195723015", + "type":"BUSINESS CURRENT", + "balance":{ + "currency":"GBP", + "amount":"15860.50" + }, + "IBAN":"BA12 1234 5123 4512 1957 2301 577", + "owners":["anil.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh", + "label":"Anil X.0.GH Red Mastercard ..869", + "number":"12691485869", + "type":"Red Mastercard", + "balance":{ + "currency":"GBP", + "amount":"7724.41" + }, + "IBAN":"BA12 1234 5123 4512 6914 8586 977", + "owners":["anil.x.0.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh", + "label":"Susan Y.9.GH Current Plus ..898", + "number":"18449011898", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"6599.63" + }, + "IBAN":"BA12 1234 5123 4518 4490 1189 877", + "owners":["susan.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh", + "label":"Robert Y.9.GH Current Plus ..251", + "number":"11875446251", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"6379.63" + }, + "IBAN":"BA12 1234 5123 4511 8754 4625 177", + "owners":["robert.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh", + "label":"Anil Y.9.GH Current Plus ..996", + "number":"10433713996", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"7588.25" + }, + "IBAN":"BA12 1234 5123 4510 4337 1399 677", + "owners":["anil.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh", + "label":"Robert Y.9.GH Current Plus ..849", + "number":"14444021849", + "type":"CURRENT PLUS", + "balance":{ + "currency":"GBP", + "amount":"6662.05" + }, + "IBAN":"BA12 1234 5123 4514 4440 2184 977", + "owners":["robert.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh", + "label":"Ellie Y.9.GH Business Current ..272", + "number":"18953434272", + "type":"BUSINESS CURRENT", + "balance":{ + "currency":"GBP", + "amount":"3748.57" + }, + "IBAN":"BA12 1234 5123 4518 9534 3427 277", + "owners":["ellie.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh", + "label":"Anil Y.9.GH Business Current ..015", + "number":"12195723015", + "type":"BUSINESS CURRENT", + "balance":{ + "currency":"GBP", + "amount":"15860.50" + }, + "IBAN":"BA12 1234 5123 4512 1957 2301 577", + "owners":["anil.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + },{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh", + "label":"Anil Y.9.GH Red Mastercard ..869", + "number":"12691485869", + "type":"Red Mastercard", + "balance":{ + "currency":"GBP", + "amount":"7724.41" + }, + "IBAN":"BA12 1234 5123 4512 6914 8586 977", + "owners":["anil.y.9.gh@example.com"], + "generate_public_view":true, + "generate_accountants_view":true, + "generate_auditors_view":true + }], + "transactions":[{ + "id":"d23e864c-c696-4188-9721-93838b3eddad", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice Mr Folk", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"27916.81", + "value":"-351.25" + } + },{ + "id":"39711f6e-20a0-4044-ae05-4f6e5af8eeb9", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice Mr Green", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"27103.46", + "value":"-813.35" + } + },{ + "id":"c2477fb3-7f95-4a88-9e46-bbf6c5e8265e", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice Miss Dean", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"23305.43", + "value":"-3798.03" + } + },{ + "id":"75d63449-9328-4d57-867b-51d2bc56dc20", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Direct Debit", + "description":"Vodafone", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"23252.06", + "value":"-53.37" + } + },{ + "id":"9dc1437a-8049-4b07-bb2c-ba6fe9ce4687", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Debit Card", + "description":"Shell Filling Station", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"23193.46", + "value":"-58.60" + } + },{ + "id":"77c2f8c9-3f5d-4859-b0bb-409ec3d6e1b3", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice 76542 Mr Green", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"22801.62", + "value":"-391.84" + } + },{ + "id":"e371d8da-aa48-453c-a3e4-18d177886e6a", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Food Place" + }, + "details":{ + "type":"Direct Debit", + "description":"The Food Place", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"22749.85", + "value":"-51.77" + } + },{ + "id":"38ef2c03-465e-47d2-b31d-bd818999f0cd", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Direct Debit", + "description":"Business insuance", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"21936.50", + "value":"-813.35" + } + },{ + "id":"ee0ff6b6-3966-40e1-8455-97b68ee786cf", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice 986 Mr Khan", + "posted":"2015-09-16T00:00:00.000Z", + "completed":"2015-09-16T00:00:00.000Z", + "new_balance":"21092.71", + "value":"-843.79" + } + },{ + "id":"3b6479d0-e228-472e-a2cf-67629183b387", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice 2297 Miss Dean", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"20248.92", + "value":"-843.79" + } + },{ + "id":"6aa7c77b-d715-4b30-8c21-791bf02efc13", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Direct Debit", + "description":"Transfer Salary", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"16995.86", + "value":"-3253.06" + } + },{ + "id":"18dccfa8-d197-40bb-869a-224fcc41e908", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"ING Mortgage" + }, + "details":{ + "type":"Direct Debit", + "description":"Mortgage", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7667.42", + "value":"-703.99" + } + },{ + "id":"f27d50fc-2381-4526-add3-022eed204b48", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Directline" + }, + "details":{ + "type":"Direct Debit", + "description":"Insurance", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7643.11", + "value":"-24.31" + } + },{ + "id":"93500326-f8d4-472d-9033-9706f2bbf5ec", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7617.03", + "value":"-26.08" + } + },{ + "id":"c5f8dab3-0331-4d5d-9a8c-d11cd2b1659b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"SSE" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7573.51", + "value":"-43.52" + } + },{ + "id":"04ae0400-bea4-43be-913e-bbc1fdf03270", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7472.51", + "value":"-101.00" + } + },{ + "id":"8db0e273-3fab-4991-b11e-0bde1873ac2d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sky" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Sky", + "posted":"2015-07-02T00:00:00.000Z", + "completed":"2015-07-02T00:00:00.000Z", + "new_balance":"7396.38", + "value":"-76.13" + } + },{ + "id":"0536d14c-f0e0-4a52-a739-fb50ee6615ba", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"7329.90", + "value":"-66.48" + } + },{ + "id":"850617c4-b361-43e8-b2ad-a894abb0e225", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Takeway King" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"7295.18", + "value":"-34.72" + } + },{ + "id":"7ecb99f9-c5a0-4cde-9500-70064568ffd4", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"7250.11", + "value":"-45.07" + } + },{ + "id":"8663199d-270a-43a5-8b81-509102005048", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"7197.09", + "value":"-53.02" + } + },{ + "id":"91b6d79e-3a2b-4b06-bf83-dfc7e2ec242e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-07-06T00:00:00.000Z", + "completed":"2015-07-06T00:00:00.000Z", + "new_balance":"7192.56", + "value":"-4.53" + } + },{ + "id":"7639ffd6-2302-42b9-b3c8-9926709c7c6b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"7148.35", + "value":"-44.21" + } + },{ + "id":"1c275d5e-cf16-445a-b172-448d53749427", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The choach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"7125.08", + "value":"-23.27" + } + },{ + "id":"356a196a-c904-458e-b245-a64a058b0df5", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Amazon" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-14T00:00:00.000Z", + "completed":"2015-07-14T00:00:00.000Z", + "new_balance":"7109.06", + "value":"-16.02" + } + },{ + "id":"83b7c3a1-69d6-40a7-907f-71ffeb9b5503", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"7099.80", + "value":"-9.26" + } + },{ + "id":"7c0121e9-e3c4-47db-9f65-6af38bd650bd", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-18T00:00:00.000Z", + "completed":"2015-07-18T00:00:00.000Z", + "new_balance":"7066.94", + "value":"-32.86" + } + },{ + "id":"26cd124d-e34a-47ab-9c6d-8c00d2a127b7", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The sandwich Factory" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"7060.87", + "value":"-6.07" + } + },{ + "id":"f6244692-9e83-4ddf-afca-b2c8a07cb4ef", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash Withdrawal", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"6779.82", + "value":"-281.05" + } + },{ + "id":"762e770e-8078-4631-aa8d-0c9216f74248", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Champagne Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-24T00:00:00.000Z", + "completed":"2015-07-24T00:00:00.000Z", + "new_balance":"6744.12", + "value":"-35.70" + } + },{ + "id":"f11b4746-0326-4a87-b885-837f712dd1f8", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Quick Bit" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-07-24T00:00:00.000Z", + "completed":"2015-07-24T00:00:00.000Z", + "new_balance":"6729.00", + "value":"-15.12" + } + },{ + "id":"788fa20d-714a-4173-826e-04eb47db8745", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-25T00:00:00.000Z", + "completed":"2015-07-25T00:00:00.000Z", + "new_balance":"6705.59", + "value":"-23.41" + } + },{ + "id":"dc1278c2-464a-409e-9a35-ca41433e2e36", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-25T00:00:00.000Z", + "completed":"2015-07-25T00:00:00.000Z", + "new_balance":"6650.01", + "value":"-55.58" + } + },{ + "id":"b29bd7c7-bdb4-41d7-95b2-7b877cd9e012", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-26T00:00:00.000Z", + "completed":"2015-07-26T00:00:00.000Z", + "new_balance":"6590.90", + "value":"-59.11" + } + },{ + "id":"ddbde74b-d9d5-4b2e-b74d-4680e009358e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-07-27T00:00:00.000Z", + "completed":"2015-07-27T00:00:00.000Z", + "new_balance":"6584.83", + "value":"-6.07" + } + },{ + "id":"72afe548-b7a7-48b6-a04f-a2cecac5f5d4", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid In", + "description":"Salary", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8608.77", + "value":"2023.94" + } + },{ + "id":"e370fb72-0b74-4df7-ab09-49522ccfbff6", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"ING Mortgage" + }, + "details":{ + "type":"Direct Debit", + "description":"Mortgage", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7904.78", + "value":"-703.99" + } + },{ + "id":"da22b2e6-1471-4e6e-83f8-bbaa6baaa43c", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Directline" + }, + "details":{ + "type":"Direct Debit", + "description":"Insurance", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7880.47", + "value":"-24.31" + } + },{ + "id":"d6166f95-15d3-42a2-82c8-d2d8eabfd14c", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7854.39", + "value":"-26.08" + } + },{ + "id":"d4e69013-b682-43fe-a8b7-321ed9ab593c", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"SSE" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7810.87", + "value":"-43.52" + } + },{ + "id":"5e8afd22-c3a1-4016-a7e8-6eb866c37f94", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7709.87", + "value":"-101.00" + } + },{ + "id":"4f3fe009-7c94-4316-822d-6d0af868c9c9", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sky" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Sky", + "posted":"2015-08-02T00:00:00.000Z", + "completed":"2015-08-02T00:00:00.000Z", + "new_balance":"7633.74", + "value":"-76.13" + } + },{ + "id":"b03b1976-7c3b-4483-b3b8-b29ee7e5b4e9", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"7620.87", + "value":"-12.87" + } + },{ + "id":"535f7e8f-1840-415f-bf05-b8816ccf923a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Quick Bit" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-08-04T00:00:00.000Z", + "completed":"2015-08-04T00:00:00.000Z", + "new_balance":"7601.32", + "value":"-19.55" + } + },{ + "id":"42487581-cbfb-4f7a-8c66-55bc58627582", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"7573.41", + "value":"-27.91" + } + },{ + "id":"48301430-ec64-4256-a4a3-ecd9c3aa15c0", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The choach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"resturant", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"7545.03", + "value":"-28.38" + } + },{ + "id":"ff103f99-e15f-4950-95d4-30975379a04f", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Showcase Cinema" + }, + "details":{ + "type":"Debit Card", + "description":"Cinema", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"7521.88", + "value":"-23.15" + } + },{ + "id":"2a205016-08c9-4869-90de-a7819d3af0e1", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-10T00:00:00.000Z", + "completed":"2015-08-10T00:00:00.000Z", + "new_balance":"7468.86", + "value":"-53.02" + } + },{ + "id":"af3fb9c6-cc18-45e0-a1ae-539a86dab395", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"7463.90", + "value":"-4.96" + } + },{ + "id":"ce60e702-d898-4345-9139-4f45065e3f32", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Pizza Express" + }, + "details":{ + "type":"Debit Card", + "description":"restaurant", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"7442.90", + "value":"-21.00" + } + },{ + "id":"236037ff-04b9-4fab-bfe0-acfa5d563040", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"7402.74", + "value":"-40.16" + } + },{ + "id":"07f8c4aa-c34b-4dac-9b6b-53e244a9e1d7", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Dominos" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-08-16T00:00:00.000Z", + "completed":"2015-08-16T00:00:00.000Z", + "new_balance":"7363.12", + "value":"-39.62" + } + },{ + "id":"aaf2b649-bfc9-4a5a-832d-211717255a72", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sainsbury's" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"7328.91", + "value":"-34.21" + } + },{ + "id":"eff26ed5-17a8-465f-87b8-7c2ae8b963d6", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-08-20T00:00:00.000Z", + "completed":"2015-08-20T00:00:00.000Z", + "new_balance":"7321.93", + "value":"-6.98" + } + },{ + "id":"bd0223e8-03aa-4773-8917-bd45a932bfa4", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The sandwich Company" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-08-20T00:00:00.000Z", + "completed":"2015-08-20T00:00:00.000Z", + "new_balance":"7315.70", + "value":"-6.23" + } + },{ + "id":"756ab4d4-d36d-443c-bb75-e0d01b4c252a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-08-22T00:00:00.000Z", + "completed":"2015-08-22T00:00:00.000Z", + "new_balance":"7358.90", + "value":"43.20" + } + },{ + "id":"ffd3d8b2-110b-4e07-81be-0d4f2843e14f", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"7288.65", + "value":"-70.25" + } + },{ + "id":"46c2363d-8cbb-4315-92c8-7b1640705856", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The choach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"7252.27", + "value":"-36.38" + } + },{ + "id":"f85e10d0-4450-407c-ab4e-d9f16c07b883", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"7229.84", + "value":"-22.43" + } + },{ + "id":"dcb14507-c0ae-4f18-a70a-28f84fa3d6ad", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Hollywood Bowl" + }, + "details":{ + "type":"Debit Card", + "description":"Bowling", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"7203.48", + "value":"-26.36" + } + },{ + "id":"533b3faa-3bf1-475d-9966-24544f84723d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid In", + "description":"Salary", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9227.42", + "value":"2023.94" + } + },{ + "id":"8b4298f2-302e-4a79-a98b-42f2b439a58d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"ING Mortgage" + }, + "details":{ + "type":"Direct Debit", + "description":"Mortgage", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8523.43", + "value":"-703.99" + } + },{ + "id":"e2d06446-976e-4f68-8dda-43968f669642", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Directline" + }, + "details":{ + "type":"Direct Debit", + "description":"Insurance", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8499.12", + "value":"-24.31" + } + },{ + "id":"c180c364-079d-4d97-9d0c-8118f1f0de1a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8473.04", + "value":"-26.08" + } + },{ + "id":"a8e9b945-1e98-4ec1-932b-7730f4e030c2", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"SSE" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8429.52", + "value":"-43.52" + } + },{ + "id":"ad964ce6-0a8e-4a0e-87ce-203e3a40cb3c", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8328.52", + "value":"-101.00" + } + },{ + "id":"936495ce-e931-4c1d-90af-0c7f38ad9dff", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sky" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Sky", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"8252.39", + "value":"-76.13" + } + },{ + "id":"0bb1b43d-1286-408a-9bda-dfe55ebd1c8d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8183.86", + "value":"-68.53" + } + },{ + "id":"86cdb706-f2f4-41de-9e70-49500e2e1810", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Sandwich Factory" + }, + "details":{ + "type":"Debit Card", + "description":"Resaurant/Takeway", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8176.75", + "value":"-7.11" + } + },{ + "id":"e5fa5335-0399-41a0-8e24-ad4d994b8268", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8135.59", + "value":"-41.16" + } + },{ + "id":"fb267d89-5d72-45ae-962a-53c07643b23a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Quick Bit" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurtant/Takeway", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8122.02", + "value":"-13.57" + } + },{ + "id":"f13670ed-a594-4286-a0fc-a4500812a5ae", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"8116.42", + "value":"-5.60" + } + },{ + "id":"70366a84-b4b3-4b05-b881-e71559db5008", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"8086.74", + "value":"-29.68" + } + },{ + "id":"24e42efb-e277-42f7-b596-56945b77f873", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-06T00:00:00.000Z", + "completed":"2015-09-06T00:00:00.000Z", + "new_balance":"8019.79", + "value":"-66.95" + } + },{ + "id":"c4cb6daa-cd5a-4807-8252-c94248ea9da7", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Harvey Nichols" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-06T00:00:00.000Z", + "completed":"2015-09-06T00:00:00.000Z", + "new_balance":"7951.26", + "value":"-68.53" + } + },{ + "id":"9020b361-aaf0-4c4f-83b8-02dc5d0f6660", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-08T00:00:00.000Z", + "completed":"2015-09-08T00:00:00.000Z", + "new_balance":"7884.31", + "value":"-66.95" + } + },{ + "id":"8fb3cff0-27ea-4fde-b0fe-45f0d349435b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Amazon" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-08T00:00:00.000Z", + "completed":"2015-09-08T00:00:00.000Z", + "new_balance":"7849.37", + "value":"-34.94" + } + },{ + "id":"10bdd8f3-da7f-433e-abc5-ff4f66d9bd18", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-10T00:00:00.000Z", + "completed":"2015-09-10T00:00:00.000Z", + "new_balance":"7844.47", + "value":"-4.90" + } + },{ + "id":"30c42a0e-cfe4-4d3a-a309-71011290919b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Topman" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-10T00:00:00.000Z", + "completed":"2015-09-10T00:00:00.000Z", + "new_balance":"7765.87", + "value":"-78.60" + } + },{ + "id":"427d852c-8637-4139-9d5f-0edbf7b5e026", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"7758.90", + "value":"-6.97" + } + },{ + "id":"d7825a81-e540-4c0a-bd3d-289a0e002817", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"7744.81", + "value":"-14.09" + } + },{ + "id":"e7528faa-29c5-403d-bb3d-ab423417f40a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Hollywood Bowling" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"7714.14", + "value":"-30.67" + } + },{ + "id":"3a060ae1-8b2b-4db8-aa0e-c8c1c70f9a59", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-13T00:00:00.000Z", + "completed":"2015-09-13T00:00:00.000Z", + "new_balance":"7673.56", + "value":"-40.58" + } + },{ + "id":"410309a6-247d-465a-bcf2-0b7b775501ea", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-16T00:00:00.000Z", + "completed":"2015-09-16T00:00:00.000Z", + "new_balance":"7668.30", + "value":"-5.26" + } + },{ + "id":"10acf185-7dbf-4d99-9750-c56577b2219d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Champagne Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"7569.02", + "value":"-99.28" + } + },{ + "id":"0151682f-d75f-422b-89c3-f13ad4d013ce", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Pizza King" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"7516.12", + "value":"-52.90" + } + },{ + "id":"7d007b17-2d2d-4d0c-bf06-e5d958ece7ce", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"7510.86", + "value":"-5.26" + } + },{ + "id":"5e19c531-eae9-4f64-bbb9-14326ee7cd5a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Chimichanga" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"7457.90", + "value":"-52.96" + } + },{ + "id":"966dff13-2205-459b-a5c2-78d66799bb25", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"7410.43", + "value":"-47.47" + } + },{ + "id":"8a09dfb4-d1c6-4904-842f-62342d65a063", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"7372.80", + "value":"-37.63" + } + },{ + "id":"95bb6ccf-001e-4170-8536-3a092c07ada1", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The coach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-20T00:00:00.000Z", + "completed":"2015-09-20T00:00:00.000Z", + "new_balance":"7315.80", + "value":"-57.00" + } + },{ + "id":"fa612f4b-de3d-4953-8f96-1a297166f197", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Dominos" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-09-20T00:00:00.000Z", + "completed":"2015-09-20T00:00:00.000Z", + "new_balance":"7262.44", + "value":"-53.36" + } + },{ + "id":"a325035c-4fc3-4298-8f42-df4c06163485", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"7255.33", + "value":"-7.11" + } + },{ + "id":"b1bd6813-6d05-4ec1-8b5f-468602953125", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Sandwich Factory" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"7250.01", + "value":"-5.32" + } + },{ + "id":"960397b6-20c2-4a70-a88a-4adb1414691d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"R J Dentist" + }, + "details":{ + "type":"Debit Card", + "description":"Dentist", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"7204.68", + "value":"-45.33" + } + },{ + "id":"82f2458b-3ff0-43b3-8543-37213db3389d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Patels Parmacy" + }, + "details":{ + "type":"Debit Card", + "description":"Parmacy", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"7196.26", + "value":"-8.42" + } + },{ + "id":"f2b9a6a3-0a4f-433c-9122-16eb6e7109e0", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"7166.84", + "value":"-29.42" + } + },{ + "id":"d78a3a7a-65e1-4313-b36a-2861eb0a08d7", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"7136.53", + "value":"-30.31" + } + },{ + "id":"2c49ea60-194f-412d-b05d-975deea0a5c5", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Takeway King" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"7121.29", + "value":"-15.24" + } + },{ + "id":"899b6971-41b8-4344-b463-a8ec57834ff3", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Mr D Moda" + }, + "details":{ + "type":"Tranfer", + "description":"Transfer", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"6840.24", + "value":"-281.05" + } + },{ + "id":"eb667ae9-8067-49d4-939b-1579bb0865c4", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-29T00:00:00.000Z", + "completed":"2015-09-29T00:00:00.000Z", + "new_balance":"6811.92", + "value":"-28.32" + } + },{ + "id":"dc830888-450e-4f11-bcff-8817af235702", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice Mr Folk", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"27944.92", + "value":"-323.14" + } + },{ + "id":"8c948fef-5b3b-43a2-889a-59d2ea73e8d7", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice Mr Green", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"26826.75", + "value":"-1118.17" + } + },{ + "id":"7d3c3354-d359-4bbe-ae22-2cf3ca762d31", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice Miss Dean", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"23063.72", + "value":"-3763.03" + } + },{ + "id":"24bfdcc1-d1de-4a86-813c-edac3aeee37e", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Direct Debit", + "description":"Vodafone", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"23010.70", + "value":"-53.02" + } + },{ + "id":"22dd5046-6191-4380-a3e3-a925af23427d", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Debit Card", + "description":"Shell Filling Station", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"22934.57", + "value":"-76.13" + } + },{ + "id":"5e7d2840-0b2c-4e39-973d-2d0337e82b37", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice 76542 Mr Green", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"22555.45", + "value":"-379.12" + } + },{ + "id":"99dbc10e-9212-4d8a-ac74-255f16020bb0", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Food Place" + }, + "details":{ + "type":"Direct Debit", + "description":"The Food Place", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"22511.93", + "value":"-43.52" + } + },{ + "id":"45446654-6328-4437-b63b-ff10a37bfc8e", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Direct Debit", + "description":"Business insuance", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"21393.76", + "value":"-1118.17" + } + },{ + "id":"9418b854-7cea-48d4-9e6a-1d8b98a5cab7", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice 986 Mr Khan", + "posted":"2015-09-16T00:00:00.000Z", + "completed":"2015-09-16T00:00:00.000Z", + "new_balance":"20726.30", + "value":"-667.46" + } + },{ + "id":"d5f75348-f687-44bd-aaa1-502e75ca356f", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Transfer", + "description":"Paid Invoice 2297 Miss Dean", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"20058.84", + "value":"-667.46" + } + },{ + "id":"2bd352ad-7c20-4526-b054-7982a6e85705", + "this_account":{ + "id":"213527de-c423-452a-b2f8-8475e4cc2cfe", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"" + }, + "details":{ + "type":"Direct Debit", + "description":"Transfer Salary", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"15860.50", + "value":"-4198.34" + } + },{ + "id":"fea9810e-d916-404c-93be-f92ed3b1f108", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4814.30", + "value":"-487.59" + } + },{ + "id":"5b09cc34-fa03-48a2-b5cc-48697e51fa8f", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Rates" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4577.14", + "value":"-237.16" + } + },{ + "id":"7723f637-1adc-4cca-b75b-b9afcfe0c102", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Southern Electric" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4315.90", + "value":"-261.24" + } + },{ + "id":"50e488b0-fb4b-4869-bdb7-30cd53f2a154", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"BT" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4186.66", + "value":"-129.24" + } + },{ + "id":"aba96e57-1361-4cbf-987f-b09fba793a32", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary Mr R Turner" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"-11.68", + "value":"-4198.34" + } + },{ + "id":"2b94f45f-4968-42c0-a9fb-5548c4cf1549", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary Mrs F White" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"-3521.15", + "value":"-3509.47" + } + },{ + "id":"c39da90e-8da4-43ee-9313-6ecf0d7c7601", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The George Hotel" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-4008.74", + "value":"-487.59" + } + },{ + "id":"97b1fac7-5730-4ffc-829d-46bf39dac53d", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Catering Company" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-4269.98", + "value":"-261.24" + } + },{ + "id":"d5af4382-73f4-448f-81f6-00a2eee5d90f", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-4313.50", + "value":"-43.52" + } + },{ + "id":"a108aec3-9eeb-49c5-bef2-03cf7770d33d", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-4368.38", + "value":"-54.88" + } + },{ + "id":"4e775735-1e62-495e-8076-b2b1613cc0a7", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation Mr and Mrs Cooper" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"-2033.41", + "value":"2334.97" + } + },{ + "id":"f54a1557-353d-404d-b6e5-c882d8858195", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation The Corner Store" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"-1803.11", + "value":"230.30" + } + },{ + "id":"faa813b8-6914-420c-b220-2e8452cedf63", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Greggs" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"-1861.81", + "value":"-58.70" + } + },{ + "id":"aa59f956-fd30-4c71-977a-0ae3394c9e0a", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Trainline" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"-1949.00", + "value":"-87.19" + } + },{ + "id":"5b79fc43-445e-4956-9fd2-9b1810fa8a15", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"-845.81", + "value":"1103.19" + } + },{ + "id":"792e53de-7f94-48e6-b15d-56bd07829f44", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation The Eye Company" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"-417.82", + "value":"427.99" + } + },{ + "id":"7d59352a-a679-4b5f-b103-3860fa0e54f9", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Orange" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"-470.84", + "value":"-53.02" + } + },{ + "id":"8a1fa0b0-710e-461e-ab4c-8eeab29ade08", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation Page Family" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"-401.07", + "value":"69.77" + } + },{ + "id":"8b823eae-6b19-4e8e-b272-1f91345d1fdb", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Royal Edinburgh hospital" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"-842.40", + "value":"-441.33" + } + },{ + "id":"a932f778-18e3-405f-8e19-1ffd285132d6", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation Mr and Mrs Waterhouse" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"-612.10", + "value":"230.30" + } + },{ + "id":"0600caa9-e19f-4618-9109-7a08ae96927e", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation The Union" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"-121.15", + "value":"490.95" + } + },{ + "id":"aebf5dc5-f8b0-4db4-ac03-a81d7a486a7c", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Rainbow hospice" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"-965.55", + "value":"-844.40" + } + },{ + "id":"71276069-8c01-4397-9956-96aac798fcfb", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation Mr James" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"1369.42", + "value":"2334.97" + } + },{ + "id":"fa785750-34eb-4597-ae9a-b75504415461", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation The Keepers inn" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"5647.90", + "value":"4278.48" + } + },{ + "id":"65aba0b6-330f-42c3-8716-b3955e64b8ac", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation Smiles" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"5878.20", + "value":"230.30" + } + },{ + "id":"7a918c54-3cb4-4fcb-99c9-4cbd388e1941", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Donation Miss Heath" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6098.86", + "value":"220.66" + } + },{ + "id":"1e74486f-8cca-4964-bd02-7f43cf03f742", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"British Gas " + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2353.27", + "value":"-84.91" + } + },{ + "id":"02a65724-869e-4475-b007-b0515f343ee6", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2320.35", + "value":"-32.92" + } + },{ + "id":"f9b33664-1ab8-4531-b885-0fb328ec8e13", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Duddingston Golf Club" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Golf membership", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2274.47", + "value":"-45.88" + } + },{ + "id":"1d0d872e-d389-42a0-8b1d-6b163eb0ed93", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2241.55", + "value":"-32.92" + } + },{ + "id":"62ade710-8303-419d-9e3b-8bd88e967896", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"2136.53", + "value":"-105.02" + } + },{ + "id":"60d73107-6aa6-40f9-b407-a1fb1279e524", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"2069.31", + "value":"-67.22" + } + },{ + "id":"2f53149e-3f5e-4c90-a0d9-234064d40e79", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-08T00:00:00.000Z", + "completed":"2015-07-08T00:00:00.000Z", + "new_balance":"2059.11", + "value":"-10.20" + } + },{ + "id":"d9bd880d-03d9-4124-9581-dee618985ee5", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Prezzo" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-07-10T00:00:00.000Z", + "completed":"2015-07-10T00:00:00.000Z", + "new_balance":"2012.31", + "value":"-46.80" + } + },{ + "id":"d959c54e-350e-497f-a5f5-610cbc940c2c", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sainsbury's" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"1970.07", + "value":"-42.24" + } + },{ + "id":"b743e72a-bf79-4e07-94b4-2a4d7e0387db", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Florist" + }, + "details":{ + "type":"Debit Card", + "description":"Florist", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"1937.15", + "value":"-32.92" + } + },{ + "id":"ea3cd082-e52f-47e2-a029-4c29e31fe977", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"1932.54", + "value":"-4.61" + } + },{ + "id":"674178d2-c3f0-4ff1-a05f-d30545fe72ab", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"NCP Car Park" + }, + "details":{ + "type":"Debit Card", + "description":"Parking", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"1928.56", + "value":"-3.98" + } + },{ + "id":"739bec6b-2694-44f5-92ae-8d26926ff194", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Kitchin" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-07-18T00:00:00.000Z", + "completed":"2015-07-18T00:00:00.000Z", + "new_balance":"1835.27", + "value":"-93.29" + } + },{ + "id":"728112c8-56b8-4174-baf6-2074c4685080", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"1783.87", + "value":"-51.40" + } + },{ + "id":"763241b7-3431-4ce3-8836-e1326cff0766", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"1716.65", + "value":"-67.22" + } + },{ + "id":"dbcae7e9-79c3-4df5-a0c6-8ce056efcad7", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bar Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-24T00:00:00.000Z", + "completed":"2015-07-24T00:00:00.000Z", + "new_balance":"1674.04", + "value":"-42.61" + } + },{ + "id":"3902f092-ab75-4f72-97f8-6e673514d140", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"British Telecom" + }, + "details":{ + "type":"Direct Debit", + "description":"BT", + "posted":"2015-07-29T00:00:00.000Z", + "completed":"2015-07-29T00:00:00.000Z", + "new_balance":"1650.33", + "value":"-23.71" + } + },{ + "id":"47bd17e5-47ed-481f-be05-f91dbcb5727f", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Paid in" + }, + "details":{ + "type":"Paid in", + "description":"Income", + "posted":"2015-07-29T00:00:00.000Z", + "completed":"2015-07-29T00:00:00.000Z", + "new_balance":"3947.71", + "value":"2297.38" + } + },{ + "id":"e736a34c-250c-435a-9b0f-ae60808d4ebd", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"RBS Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-07-30T00:00:00.000Z", + "completed":"2015-07-30T00:00:00.000Z", + "new_balance":"3579.70", + "value":"-368.01" + } + },{ + "id":"b0def39a-7be8-43df-aee7-2807fbbd985d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"British Gas " + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3494.79", + "value":"-84.91" + } + },{ + "id":"54d8fd71-eb8f-4817-b1f8-c7c1c66f4bae", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3461.87", + "value":"-32.92" + } + },{ + "id":"d930534b-eeb7-4920-9452-0741f947caa9", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Duddingston Golf Club" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Golf membership", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3415.99", + "value":"-45.88" + } + },{ + "id":"11558c49-92d9-459b-9eec-eafac67de628", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3383.07", + "value":"-32.92" + } + },{ + "id":"3e6fafb8-946e-4023-b3d6-0e50bde25fe7", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The News Shop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"3377.18", + "value":"-5.89" + } + },{ + "id":"39da0fa8-d355-4604-be1c-6d12b8b4386f", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-04T00:00:00.000Z", + "completed":"2015-08-04T00:00:00.000Z", + "new_balance":"3309.96", + "value":"-67.22" + } + },{ + "id":"2ee35cfc-fef9-4b79-9874-e7f7843c9013", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3297.64", + "value":"-12.32" + } + },{ + "id":"35f89966-286c-40d5-ac58-b30ec2dd5b63", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Next" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3266.15", + "value":"-31.49" + } + },{ + "id":"29989501-6a6f-4314-a3df-86d699843c75", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"House of Fraser" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3163.27", + "value":"-102.88" + } + },{ + "id":"64432adf-8e00-4597-9769-acf032ee67a2", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"La Favorita Restaurant" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3086.56", + "value":"-76.71" + } + },{ + "id":"3acbf423-8d3c-4451-84a5-19a974810c7c", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"3037.79", + "value":"-48.77" + } + },{ + "id":"db764abb-aeff-467c-861e-164013ddc7b7", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bar Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"3009.91", + "value":"-27.88" + } + },{ + "id":"e4ec77b1-d4d4-4035-8ecf-a227565bd149", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Prezzo" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"2978.51", + "value":"-31.40" + } + },{ + "id":"9d1b5222-af5a-4da5-9fa3-626390fcf1d1", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-19T00:00:00.000Z", + "completed":"2015-08-19T00:00:00.000Z", + "new_balance":"2973.37", + "value":"-5.14" + } + },{ + "id":"38af4651-f4ea-4f3d-971e-128dbaaa15a6", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-22T00:00:00.000Z", + "completed":"2015-08-22T00:00:00.000Z", + "new_balance":"2921.05", + "value":"-52.32" + } + },{ + "id":"550028d2-c0af-4aec-be22-41fde336d713", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-26T00:00:00.000Z", + "completed":"2015-08-26T00:00:00.000Z", + "new_balance":"2853.83", + "value":"-67.22" + } + },{ + "id":"3f6b8355-4ab1-4272-895c-ce56922237b7", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"British Telecom" + }, + "details":{ + "type":"Direct Debit", + "description":"BT", + "posted":"2015-08-29T00:00:00.000Z", + "completed":"2015-08-29T00:00:00.000Z", + "new_balance":"2830.12", + "value":"-23.71" + } + },{ + "id":"244fce61-b9e7-489d-9ed5-bf3f6f62783c", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Paid in" + }, + "details":{ + "type":"Paid in", + "description":"Income", + "posted":"2015-08-29T00:00:00.000Z", + "completed":"2015-08-29T00:00:00.000Z", + "new_balance":"5127.50", + "value":"2297.38" + } + },{ + "id":"162fb4a5-48c0-4c53-817b-90ddd0437d68", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"RBS Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-08-30T00:00:00.000Z", + "completed":"2015-08-30T00:00:00.000Z", + "new_balance":"4759.49", + "value":"-368.01" + } + },{ + "id":"9cbb9fa8-e9a4-4f28-95fc-4fcfedc0521c", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"British Gas" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4674.58", + "value":"-84.91" + } + },{ + "id":"39541afe-ed7e-4b0f-b433-fa6b10e01e9f", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4641.66", + "value":"-32.92" + } + },{ + "id":"4641d5ef-3995-43d5-8390-fe1d3e546a9e", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Duddingston Golf Club" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Golf membership", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4595.78", + "value":"-45.88" + } + },{ + "id":"57b0f7f6-04f7-4c8b-ae94-b07e525f1d74", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4562.86", + "value":"-32.92" + } + },{ + "id":"fa647c36-ae45-4cba-82e1-184924124036", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"4510.75", + "value":"-52.11" + } + },{ + "id":"f8d2e8f2-e2d6-4f68-9a33-e7429758b5f2", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"4436.97", + "value":"-73.78" + } + },{ + "id":"d62da697-3b57-4ebb-868d-37dbe96ca3d2", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Cellar Door" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"4343.76", + "value":"-93.21" + } + },{ + "id":"de73f9b1-5884-4626-a712-3247904708ee", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4336.65", + "value":"-7.11" + } + },{ + "id":"aac84f86-1e83-4a37-baef-bbe98e83601b", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Debenhams" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4257.75", + "value":"-78.90" + } + },{ + "id":"47fa7045-33d1-4efe-8fdd-df9ff06e4a6d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Next" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4208.54", + "value":"-49.21" + } + },{ + "id":"00fe8a25-444c-4330-9391-6dd1c049e75d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Pizza Express" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4177.31", + "value":"-31.23" + } + },{ + "id":"4c96bc6f-f855-42f1-bcd9-9b5564dd7470", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-22T00:00:00.000Z", + "completed":"2015-09-22T00:00:00.000Z", + "new_balance":"4105.42", + "value":"-71.89" + } + },{ + "id":"692c2fcc-2aa1-40fd-bc21-7eb2d526d3ea", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"British Telecom" + }, + "details":{ + "type":"Direct Debit", + "description":"BT", + "posted":"2015-09-29T00:00:00.000Z", + "completed":"2015-09-29T00:00:00.000Z", + "new_balance":"4081.71", + "value":"-23.71" + } + },{ + "id":"51b9aa86-009f-4005-9d01-d8240109e4eb", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Paid in" + }, + "details":{ + "type":"Paid in", + "description":"Income", + "posted":"2015-09-29T00:00:00.000Z", + "completed":"2015-09-29T00:00:00.000Z", + "new_balance":"6379.09", + "value":"2297.38" + } + },{ + "id":"8a27641b-a866-4081-a8f8-ac53171af5fa", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"RBS Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6011.08", + "value":"-368.01" + } + },{ + "id":"d94dd841-038f-45ce-b1c5-81da8b419673", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Credit Card", + "description":"Bar", + "posted":"2010-09-27T00:00:00.000Z", + "completed":"2010-09-27T00:00:00.000Z", + "new_balance":"8396.28", + "value":"-14.76" + } + },{ + "id":"9285476b-9d50-4d65-9697-9a662f959dd8", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8330.72", + "value":"-65.56" + } + },{ + "id":"33a4f625-73b0-495c-a404-b711e665cd35", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco Filling Station" + }, + "details":{ + "type":"Credit Card", + "description":"Filling Station", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"8277.35", + "value":"-53.37" + } + },{ + "id":"2f5bf0e6-eadd-4a15-9a61-9e443263bdb7", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Credit Card", + "description":"Bar", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"8207.55", + "value":"-69.80" + } + },{ + "id":"4a582e83-e2b2-426e-9308-f8afcfd4c256", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Dominos" + }, + "details":{ + "type":"Credit Card", + "description":"Takeway", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"8200.47", + "value":"-7.08" + } + },{ + "id":"ec77fefe-a8de-4b1c-9f54-adbbc65cf99b", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Credit Card", + "description":"coffee", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"8197.13", + "value":"-3.34" + } + },{ + "id":"58eab15b-3ad1-4708-8632-fc6e8f3a18d7", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Prezzo" + }, + "details":{ + "type":"Credit Card", + "description":"resturant", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"8169.90", + "value":"-27.23" + } + },{ + "id":"a6bf0706-64b3-49c7-bb78-c812ebce4111", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"H Samuals" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"7801.89", + "value":"-368.01" + } + },{ + "id":"f163dd22-f4ae-419a-8f93-28f753b2be33", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Zara" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"7687.30", + "value":"-114.59" + } + },{ + "id":"0b3471b2-a33e-4f7f-a95c-463cf6c65ced", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Credit Card", + "description":"coffee", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"7683.96", + "value":"-3.34" + } + },{ + "id":"696c0dc4-0dd6-4ee8-a28b-2590e0c0fd69", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Showcase Cinema" + }, + "details":{ + "type":"Credit Card", + "description":"Cinema", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"7662.07", + "value":"-21.89" + } + },{ + "id":"ca214ab9-3cbb-4778-9e3e-98c3a9bdb161", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"7596.51", + "value":"-65.56" + } + },{ + "id":"b88d2abe-2e6a-4f02-be72-0607615c21d5", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"NCP Car Park" + }, + "details":{ + "type":"Credit Card", + "description":"Parking", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"7589.43", + "value":"-7.08" + } + },{ + "id":"2d72f6eb-98c5-47bf-bf3b-2c3329c42c82", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7580.99", + "value":"-90.58" + } + },{ + "id":"b47a522a-38df-4dc3-90ba-19a535385f1d", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"Rent", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6877.00", + "value":"-703.99" + } + },{ + "id":"2882a8be-0b36-4218-937d-a12fca1e38cd", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"EON" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6823.98", + "value":"-53.02" + } + },{ + "id":"18d872ea-09dc-43fd-83e7-7b0dd64c7344", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6799.67", + "value":"-24.31" + } + },{ + "id":"c50427f0-b65d-4b91-a7c0-67f272629ba4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Talk Talk" + }, + "details":{ + "type":"Direct Debit", + "description":"Telephone", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6768.72", + "value":"-30.95" + } + },{ + "id":"7b4a9679-eba7-42b2-8dce-e53b423adff4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"6723.17", + "value":"-45.55" + } + },{ + "id":"d8008dc6-b9b6-41d7-a288-d4008af079c4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"6679.65", + "value":"-43.52" + } + },{ + "id":"fc879488-4189-41d5-bf6a-7dbaab9035ce", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Poundland" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"6666.29", + "value":"-13.36" + } + },{ + "id":"e73d9b77-aeb9-4f0c-90f1-f6a19dfdae06", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Specsavers" + }, + "details":{ + "type":"Debit Card", + "description":"Optician", + "posted":"2015-07-06T00:00:00.000Z", + "completed":"2015-07-06T00:00:00.000Z", + "new_balance":"6640.21", + "value":"-26.08" + } + },{ + "id":"e02f3e08-78db-433d-895b-536b5fb16d45", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-06T00:00:00.000Z", + "completed":"2015-07-06T00:00:00.000Z", + "new_balance":"6612.06", + "value":"-28.15" + } + },{ + "id":"0df0e27d-692d-4418-a25f-32bc4d0b6692", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-10T00:00:00.000Z", + "completed":"2015-07-10T00:00:00.000Z", + "new_balance":"6568.54", + "value":"-43.52" + } + },{ + "id":"de1fc929-226e-44b1-9e2e-65c97703249a", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-10T00:00:00.000Z", + "completed":"2015-07-10T00:00:00.000Z", + "new_balance":"6541.37", + "value":"-27.17" + } + },{ + "id":"90855b41-d5c6-418a-8863-aae7fd2b833b", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"6503.24", + "value":"-38.13" + } + },{ + "id":"ef5c95bc-816f-4189-9eaa-8a7d51f11c27", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Clarks" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"6472.29", + "value":"-30.95" + } + },{ + "id":"18ed6025-e0f6-422f-a030-7ab7d7b76fdc", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Next" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"6445.88", + "value":"-26.41" + } + },{ + "id":"17ca9b0b-574a-4c37-bcd4-15747c758d7c", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-15T00:00:00.000Z", + "completed":"2015-07-15T00:00:00.000Z", + "new_balance":"6425.13", + "value":"-20.75" + } + },{ + "id":"0dbfc11b-e48f-4fcf-af3e-fcb852851540", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"6381.61", + "value":"-43.52" + } + },{ + "id":"74486fc8-ab0c-44ac-9f4b-2db1cb4d11c4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"6373.46", + "value":"-8.15" + } + },{ + "id":"ee38dffc-6e38-4cce-95e9-1c9d30c4fe54", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"6300.36", + "value":"-73.10" + } + },{ + "id":"716c9acc-3eda-4ce9-bea5-7dd70deb21c7", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling station", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"6272.08", + "value":"-28.28" + } + },{ + "id":"7c175892-a805-4d86-8451-d130912be291", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Pizza hut" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-19T00:00:00.000Z", + "completed":"2015-07-19T00:00:00.000Z", + "new_balance":"6232.03", + "value":"-40.05" + } + },{ + "id":"ec2e04c4-3983-4bad-a97c-5fc4c204a730", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"6188.51", + "value":"-43.52" + } + },{ + "id":"14044493-c1cf-47ef-b11a-58011b1413de", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-23T00:00:00.000Z", + "completed":"2015-07-23T00:00:00.000Z", + "new_balance":"6175.30", + "value":"-13.21" + } + },{ + "id":"1dd9f666-7a42-4ad7-916d-e0b3246d1052", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-26T00:00:00.000Z", + "completed":"2015-07-26T00:00:00.000Z", + "new_balance":"6125.43", + "value":"-49.87" + } + },{ + "id":"b7945ecb-060a-4e6d-8b52-a4f999a7f6c6", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-07-27T00:00:00.000Z", + "completed":"2015-07-27T00:00:00.000Z", + "new_balance":"7336.38", + "value":"1210.95" + } + },{ + "id":"6f71b200-0b9d-44a8-948c-b0c5e8bc411a", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"three" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile phone", + "posted":"2015-07-28T00:00:00.000Z", + "completed":"2015-07-28T00:00:00.000Z", + "new_balance":"7312.07", + "value":"-24.31" + } + },{ + "id":"84c3ccbd-29aa-4cce-8e76-dceeb083a0ca", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Post Office Savings Account" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-07-30T00:00:00.000Z", + "completed":"2015-07-30T00:00:00.000Z", + "new_balance":"7277.13", + "value":"-34.94" + } + },{ + "id":"f85bcbd0-923c-4511-82e0-f3740f112dc4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7186.55", + "value":"-90.58" + } + },{ + "id":"514f998a-3802-4df5-9528-9c1146702daf", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"Rent", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6482.56", + "value":"-703.99" + } + },{ + "id":"31e97c98-9181-47c3-8886-a56f0e3c7f1d", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"EON" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6429.54", + "value":"-53.02" + } + },{ + "id":"a4c8049b-2db3-444b-9e7f-74d538f09b4d", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6405.23", + "value":"-24.31" + } + },{ + "id":"2a55ff3d-f3c3-4ed9-9875-e3b95ba769ee", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Talk Talk" + }, + "details":{ + "type":"Direct Debit", + "description":"Telephone", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6374.28", + "value":"-30.95" + } + },{ + "id":"e7b9ae7a-9741-4f91-bc8b-57ce088d885e", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"6328.73", + "value":"-45.55" + } + },{ + "id":"05fa5bec-cd11-4e5c-bbdd-00191e147e7f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"6285.21", + "value":"-43.52" + } + },{ + "id":"7bc16eb8-826a-4d09-ae94-540d8b7762fe", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-13T00:00:00.000Z", + "completed":"2015-08-13T00:00:00.000Z", + "new_balance":"6259.13", + "value":"-26.08" + } + },{ + "id":"0ecbe794-8606-4e63-bf41-f198ba67ce01", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Ebay" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"6218.97", + "value":"-40.16" + } + },{ + "id":"88b1b578-74cf-4653-8630-f37fa15630b1", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Book people" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"6208.17", + "value":"-10.80" + } + },{ + "id":"4cdff543-cd92-4b0c-be93-675c0723c533", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Domino's" + }, + "details":{ + "type":"Debit Card", + "description":"Takeaway", + "posted":"2015-08-21T00:00:00.000Z", + "completed":"2015-08-21T00:00:00.000Z", + "new_balance":"6175.98", + "value":"-32.19" + } + },{ + "id":"987bfa02-b398-44dc-8aab-537d3ad12607", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"6134.57", + "value":"-41.41" + } + },{ + "id":"d1a13b55-3d72-4612-9ace-f7133dd3bca9", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"6091.05", + "value":"-43.52" + } + },{ + "id":"f8033790-1b8c-4e93-94c0-bd1951669abe", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Argos" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-24T00:00:00.000Z", + "completed":"2015-08-24T00:00:00.000Z", + "new_balance":"6080.25", + "value":"-10.80" + } + },{ + "id":"2081deac-b982-4c8b-9212-9c44ced9574a", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-27T00:00:00.000Z", + "completed":"2015-08-27T00:00:00.000Z", + "new_balance":"6062.47", + "value":"-17.78" + } + },{ + "id":"42dda6ca-a3ac-4563-826a-4215677510b0", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-27T00:00:00.000Z", + "completed":"2015-08-27T00:00:00.000Z", + "new_balance":"6012.60", + "value":"-49.87" + } + },{ + "id":"d4b90f87-7b4d-46a9-924f-9e001c6af73f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-08-27T00:00:00.000Z", + "completed":"2015-08-27T00:00:00.000Z", + "new_balance":"7223.55", + "value":"1210.95" + } + },{ + "id":"427607a3-18e0-42c2-9911-9a4511c6fa1a", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"three" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile phone", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"7199.24", + "value":"-24.31" + } + },{ + "id":"e674975a-e61b-474e-b039-a6eadd44b8cb", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Post Office Savings Account" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-08-30T00:00:00.000Z", + "completed":"2015-08-30T00:00:00.000Z", + "new_balance":"7164.30", + "value":"-34.94" + } + },{ + "id":"8f067a5d-d25f-4ed6-acc2-88488672bc0b", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"7073.72", + "value":"-90.58" + } + },{ + "id":"3e4804f1-3518-4c10-a3c9-a13171aa8769", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"Rent", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6369.73", + "value":"-703.99" + } + },{ + "id":"7b3fd0bb-3ef2-4664-a10b-ed4a789170db", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"EON" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6316.71", + "value":"-53.02" + } + },{ + "id":"9311ce64-2c2c-4dcd-84a8-f97c3f48fa3e", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6292.40", + "value":"-24.31" + } + },{ + "id":"9688adec-4597-4901-a30b-5e00bd69dd1f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Talk Talk" + }, + "details":{ + "type":"Direct Debit", + "description":"Telephone", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6261.45", + "value":"-30.95" + } + },{ + "id":"d8bb368f-b066-4119-93c9-2da47fb9b329", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"6216.90", + "value":"-44.55" + } + },{ + "id":"164921fe-e182-489a-8fe1-40ecf0e2ffbc", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"ATM Asda" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash withdrawal", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"6173.38", + "value":"-43.52" + } + },{ + "id":"9e26be31-11fd-4f5d-931f-f5b17e638c5a", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"6151.61", + "value":"-21.77" + } + },{ + "id":"a6602093-5b3d-406c-8df0-d962920ad851", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"ATM High Street" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash withdrawal", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"6108.09", + "value":"-43.52" + } + },{ + "id":"bb22c6e1-ae9a-412e-bb54-afa8e794784c", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"6091.79", + "value":"-16.30" + } + },{ + "id":"de7b6f3b-95a9-4409-90b3-e66571b2f2ec", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-13T00:00:00.000Z", + "completed":"2015-09-13T00:00:00.000Z", + "new_balance":"6030.24", + "value":"-61.55" + } + },{ + "id":"5ca49db0-49a9-44e1-856e-a4c68c0273c9", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Poundland" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-14T00:00:00.000Z", + "completed":"2015-09-14T00:00:00.000Z", + "new_balance":"6019.81", + "value":"-10.43" + } + },{ + "id":"a70e9f04-6a6b-48ba-8846-f61ad0c572b9", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling station", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"5984.87", + "value":"-34.94" + } + },{ + "id":"9d2dcd29-165f-482c-806d-a8d4cda67abe", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sport Direct" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"5952.53", + "value":"-32.34" + } + },{ + "id":"9c2932ac-4e57-4f86-80b9-5a5f42563270", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Pizza Hut" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"5908.66", + "value":"-43.87" + } + },{ + "id":"da467994-5700-4331-80c5-c295b6588b97", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"5888.17", + "value":"-20.49" + } + },{ + "id":"c73f0491-c216-47fe-86b7-4997b47774f2", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Paid in", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"7099.12", + "value":"1210.95" + } + },{ + "id":"882eef84-b366-46c4-86ca-6984e4cf2389", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"7046.89", + "value":"-52.23" + } + },{ + "id":"a2322b18-3834-44f3-996b-fb089597e3c5", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash withdrawal", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"7003.37", + "value":"-43.52" + } + },{ + "id":"cbf37ad1-0389-40e4-a432-4ceeacfba5fb", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"three" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile phone", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"6979.06", + "value":"-24.31" + } + },{ + "id":"2ce6fc59-1165-426b-8b6f-d8695c4028c9", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Post Office Savings Account" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6944.12", + "value":"-34.94" + } + },{ + "id":"9d8ae590-8e5e-407f-8c45-e92d0efa054b", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Asda Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling station", + "posted":"2025-08-08T00:00:00.000Z", + "completed":"2025-08-08T00:00:00.000Z", + "new_balance":"6917.75", + "value":"-26.37" + } + },{ + "id":"d33f53ae-e4e3-4ee9-afd5-4f15c083eb60", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4724.26", + "value":"-577.63" + } + },{ + "id":"7366d126-927e-4275-8c17-bfb4a531fd50", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Rates" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4496.22", + "value":"-228.04" + } + },{ + "id":"71a6734f-81a1-4da5-9cb8-9a945c1cc7be", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Southern Electric" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4128.21", + "value":"-368.01" + } + },{ + "id":"63b67732-0f5f-4e7d-b710-c20f9b418a45", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"BT" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4013.62", + "value":"-114.59" + } + },{ + "id":"a4f3ecef-b847-4de8-b922-45f6d3006bb4", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary Mr R Turner" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"760.56", + "value":"-3253.06" + } + },{ + "id":"31ed3fde-11a4-4209-94e4-826507ab2336", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary Mrs F White" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"-2551.89", + "value":"-3312.45" + } + },{ + "id":"e2a0dc41-e621-4838-882f-0d04976fd551", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The George Hotel" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-3129.52", + "value":"-577.63" + } + },{ + "id":"fb49007f-bff6-4852-a2e8-81dbddc18f70", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Catering Company" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-3497.53", + "value":"-368.01" + } + },{ + "id":"9eae8f86-884a-4ca5-8115-f0946df723d8", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-3549.30", + "value":"-51.77" + } + },{ + "id":"e3daab67-ae45-4bd6-a4ed-1178c5423cae", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"-3595.18", + "value":"-45.88" + } + },{ + "id":"a5c4d35d-20e5-4e16-bf82-385596b82356", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation Mr and Mrs Cooper" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"-1412.09", + "value":"2183.09" + } + },{ + "id":"8e041d48-26b4-4d2e-bc1f-2a8eaa436424", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation The Corner Store" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"-1137.31", + "value":"274.78" + } + },{ + "id":"d58425e2-285b-4a8b-b3e6-2d6227112e18", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Greggs" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"-1207.11", + "value":"-69.80" + } + },{ + "id":"c5a8e2d7-7072-4b62-a2f6-2dad68ea1ba0", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Trainline" + }, + "details":{ + "type":"Debit Card", + "description":"", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"-1284.40", + "value":"-77.29" + } + },{ + "id":"71464e7d-7c07-42c8-a00a-dcad61d24d23", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"-189.30", + "value":"1095.10" + } + },{ + "id":"7e48e8b3-efbf-41c9-9757-2654b2f66c4b", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation The Eye Company" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"260.14", + "value":"449.44" + } + },{ + "id":"f017a08f-d5d1-4346-9b48-42664cd5c2c5", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Orange" + }, + "details":{ + "type":"Direct Debit", + "description":"", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"206.77", + "value":"-53.37" + } + },{ + "id":"70e76553-0282-4424-9b9b-e463ee0e3d26", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation Page Family" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"262.20", + "value":"55.43" + } + },{ + "id":"9fd82db7-b69b-4ce8-b89b-95a4ee5300f2", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Royal Edinburgh hospital" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"-357.39", + "value":"-619.59" + } + },{ + "id":"18841ccc-fc60-4f12-9b9b-dc244ab5fe44", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation Mr and Mrs Waterhouse" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"-82.61", + "value":"274.78" + } + },{ + "id":"284e6451-3e79-41cc-97b3-e0d8bd856903", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation The Union" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"394.69", + "value":"477.30" + } + },{ + "id":"10e48400-9476-4898-bdf7-9d6e956d1f31", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Rainbow hospice" + }, + "details":{ + "type":"Transfer", + "description":"", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"-554.53", + "value":"-949.22" + } + },{ + "id":"1aee29cc-1e09-413b-ba89-a875d4f01b80", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation Mr James" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"1628.56", + "value":"2183.09" + } + },{ + "id":"6e2f67af-8a38-4ec9-a9b1-291286371ec7", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation The Keepers inn" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"6148.90", + "value":"4520.34" + } + },{ + "id":"7f9fffda-5dd7-4810-9eff-b49ba27e79ec", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation Smiles" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6423.68", + "value":"274.78" + } + },{ + "id":"04135e2b-6489-4d56-bfb5-f7bcc8c4d3d1", + "this_account":{ + "id":"e4f001fe-0f0d-4f93-a8b2-d865077315ec", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Donation Miss Heath" + }, + "details":{ + "type":"Paid in", + "description":"", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6662.05", + "value":"238.37" + } + },{ + "id":"48242826-7765-49a8-ab9e-69c58c3e1acb", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"3276.25", + "value":"1192.31" + } + },{ + "id":"7ce3ff34-1c52-4401-909c-d6d71c8bfa21", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid In", + "description":"Salary", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"5547.90", + "value":"2271.65" + } + },{ + "id":"20f346a9-855f-4c3b-b961-211ab6e38b68", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Car Plan" + }, + "details":{ + "type":"Direct Debit", + "description":"Car Monthly Payment", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"5237.96", + "value":"-309.94" + } + },{ + "id":"225866a6-e6ac-4f6b-b9b2-951e7d55b1ef", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"New Look" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"5197.61", + "value":"-40.35" + } + },{ + "id":"03c777d8-6d68-4c10-b5c1-7a183460cbf3", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Burger King" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"5188.76", + "value":"-8.85" + } + },{ + "id":"5d07321d-33ff-4659-8c51-8056f27eb5f9", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"5164.25", + "value":"-24.51" + } + },{ + "id":"97258c22-d49e-4246-bf5e-d54fe17ff577", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"5154.05", + "value":"-10.20" + } + },{ + "id":"76677ba7-e0f2-4ea5-bf13-2faf88635894", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-07T00:00:00.000Z", + "completed":"2015-07-07T00:00:00.000Z", + "new_balance":"5148.91", + "value":"-5.14" + } + },{ + "id":"7fb7374c-7ea9-41f4-9a8d-a3dddd6fcdcc", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Body Shop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"5136.24", + "value":"-12.67" + } + },{ + "id":"0cd1836a-658c-4797-9772-a94f657ffb05", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"5116.22", + "value":"-20.02" + } + },{ + "id":"1ad7601b-8f3f-4ee3-bdee-1c85a66d6749", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nail Art" + }, + "details":{ + "type":"Debit Card", + "description":"Nails", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"5091.39", + "value":"-24.83" + } + },{ + "id":"323c7a7d-7c8f-45f7-a41e-db199b6bb840", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Terrance Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"5080.57", + "value":"-10.82" + } + },{ + "id":"2db52f7f-8b87-4585-8b24-e06c88aff46d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Zizzi" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"5050.19", + "value":"-30.38" + } + },{ + "id":"8a586223-85f4-4be1-ba44-62f27e096644", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"4989.21", + "value":"-60.98" + } + },{ + "id":"bec9defa-7cdb-4346-8097-27b889707c86", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station ATM" + }, + "details":{ + "type":"Debit Card", + "description":"Cash Withdrawals", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"4904.30", + "value":"-84.91" + } + },{ + "id":"73352def-b83c-4c53-aa4f-4ca2c2879bcd", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"4858.69", + "value":"-45.61" + } + },{ + "id":"0cc14465-5061-4c85-9eb6-9d4c83a60d40", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"4851.29", + "value":"-7.40" + } + },{ + "id":"8815b4b6-a874-486a-a227-aa4aaddc11e2", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"4841.79", + "value":"-9.50" + } + },{ + "id":"2392d4b6-3657-4cc2-910f-7797f1045405", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Salon" + }, + "details":{ + "type":"Debit Card", + "description":"Hair", + "posted":"2015-07-19T00:00:00.000Z", + "completed":"2015-07-19T00:00:00.000Z", + "new_balance":"4764.50", + "value":"-77.29" + } + },{ + "id":"c7610246-9e56-47c3-b9bd-c51025ea913d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-21T00:00:00.000Z", + "completed":"2015-07-21T00:00:00.000Z", + "new_balance":"4741.02", + "value":"-23.48" + } + },{ + "id":"de343718-f13d-4642-ab2c-5489cc30868c", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-23T00:00:00.000Z", + "completed":"2015-07-23T00:00:00.000Z", + "new_balance":"4703.02", + "value":"-38.00" + } + },{ + "id":"41106e95-7e1a-4a88-9a35-52b551ec44ef", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-25T00:00:00.000Z", + "completed":"2015-07-25T00:00:00.000Z", + "new_balance":"4688.21", + "value":"-14.81" + } + },{ + "id":"33c6a107-156d-4520-a8c1-57fda9eb99d1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-27T00:00:00.000Z", + "completed":"2015-07-27T00:00:00.000Z", + "new_balance":"4683.07", + "value":"-5.14" + } + },{ + "id":"4ce3b989-a5d4-4811-8ef4-68a0210025a2", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Orange Mobile" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile", + "posted":"2015-07-28T00:00:00.000Z", + "completed":"2015-07-28T00:00:00.000Z", + "new_balance":"4633.14", + "value":"-49.93" + } + },{ + "id":"4aa788f4-c1df-4d13-98f1-f6f55cd00da8", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Saving", + "posted":"2015-07-28T00:00:00.000Z", + "completed":"2015-07-28T00:00:00.000Z", + "new_balance":"4405.10", + "value":"-228.04" + } + },{ + "id":"8b57174e-35d5-488c-b72e-9285ef62fe19", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"5597.41", + "value":"1192.31" + } + },{ + "id":"2f2c19e6-ba2b-4a9d-98a0-4ffecbb3f083", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Car Plan" + }, + "details":{ + "type":"Direct Debit", + "description":"Car Monthly Payment", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"5287.47", + "value":"-309.94" + } + },{ + "id":"c28e16be-d221-45eb-a16c-920fbe09dd9f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash Withdrawals", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"5172.88", + "value":"-114.59" + } + },{ + "id":"faef693a-756e-4d42-930d-33601c7ee83f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-04T00:00:00.000Z", + "completed":"2015-08-04T00:00:00.000Z", + "new_balance":"5158.65", + "value":"-14.23" + } + },{ + "id":"033e07d6-c895-4201-95b9-6e0c8b65a66a", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"5134.39", + "value":"-24.26" + } + },{ + "id":"5ce4b4d0-042c-4d86-8685-d9fa53ff320e", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Zizzi" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"5104.01", + "value":"-30.38" + } + },{ + "id":"50fb157b-f4f0-4ed3-a33a-43410d65c476", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Netflix" + }, + "details":{ + "type":"Debit Card", + "description":"Monthly Netflix Membership", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"5096.95", + "value":"-7.06" + } + },{ + "id":"fa626c9f-4813-40f1-aec0-1adf60940de6", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Topshop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"5041.07", + "value":"-55.88" + } + },{ + "id":"07d4635a-4358-4c29-8e41-1b37b69e3e68", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Hand Made Burger Company" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"5003.23", + "value":"-37.84" + } + },{ + "id":"27983a4c-9485-4670-bc02-7900c258e152", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash Withdrawals", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"4918.32", + "value":"-84.91" + } + },{ + "id":"4760d1dc-a55f-4241-9f43-55d6b8023700", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"4892.71", + "value":"-25.61" + } + },{ + "id":"5c917149-dd6d-42d9-bc19-d539392a6461", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-11T00:00:00.000Z", + "completed":"2015-08-11T00:00:00.000Z", + "new_balance":"4887.57", + "value":"-5.14" + } + },{ + "id":"316b4b70-7bdd-4ceb-aa3b-2f893f936a1f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Terrance Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"4853.62", + "value":"-33.95" + } + },{ + "id":"a4084c76-ce31-4ea8-b4da-47ee0242dae7", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"4823.24", + "value":"-30.38" + } + },{ + "id":"eeea5a0a-25c2-4c5a-ad2f-4cc7b6a09dc0", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"4757.68", + "value":"-65.56" + } + },{ + "id":"73e7fb56-78fb-4f8c-b243-484a0c9bf119", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"4750.28", + "value":"-7.40" + } + },{ + "id":"15eca993-1388-445a-8829-cde8007d1f7c", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Aiport Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-19T00:00:00.000Z", + "completed":"2015-08-19T00:00:00.000Z", + "new_balance":"4742.54", + "value":"-7.74" + } + },{ + "id":"58550700-1f7d-4dbe-a29d-37cee2c2c49b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Orange Mobile" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"4692.61", + "value":"-49.93" + } + },{ + "id":"3bd48297-b26f-49ab-b7c1-98d26f70cd25", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Saving", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"4464.57", + "value":"-228.04" + } + },{ + "id":"b7232c34-7cd4-4881-a453-e250baee1655", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"5656.88", + "value":"1192.31" + } + },{ + "id":"bfc66595-4215-4298-82d3-42c0c4201d77", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Car Plan" + }, + "details":{ + "type":"Direct Debit", + "description":"Car Monthly Payment", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"5346.94", + "value":"-309.94" + } + },{ + "id":"d39f65f8-09fc-42b3-90db-fa0ce4f89317", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Zara" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5281.38", + "value":"-65.56" + } + },{ + "id":"85c0e6dc-b922-4cab-afa4-e75781954f60", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5239.89", + "value":"-41.49" + } + },{ + "id":"cb485e1c-1941-4fa7-aba5-df0913ab7990", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5216.58", + "value":"-23.31" + } + },{ + "id":"036ac198-52c4-4b6b-9d01-c6d8b6157848", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Zizzi" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5178.58", + "value":"-38.00" + } + },{ + "id":"04c10d10-914a-4cb1-bec7-c335d048528d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5171.18", + "value":"-7.40" + } + },{ + "id":"5ab85893-d3ce-4c22-b059-a46e795bf7ed", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Ticket Master" + }, + "details":{ + "type":"Debit Card", + "description":"Tickets", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5104.35", + "value":"-66.83" + } + },{ + "id":"206c9b33-1aee-4c7e-8ebb-9d3d94a6833f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Salon" + }, + "details":{ + "type":"Debit Card", + "description":"Hair", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5025.45", + "value":"-78.90" + } + },{ + "id":"e5f3d1a5-ef1b-497f-ab1d-d65185771b82", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"4980.20", + "value":"-45.25" + } + },{ + "id":"cb4b2cc8-f64a-4b45-87df-67f649e94575", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash Withdrawals", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"4895.29", + "value":"-84.91" + } + },{ + "id":"8ae09f1d-fd03-4fb9-b647-c757dfceecc9", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Showcase Cinema" + }, + "details":{ + "type":"Debit Card", + "description":"Cinema", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"4885.40", + "value":"-9.89" + } + },{ + "id":"14d985be-60e6-4eb5-bfb5-920b1bf2fd82", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Netflix" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Netflix Membership", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4879.94", + "value":"-5.46" + } + },{ + "id":"7b6b97b4-4520-4cbc-b309-51f572ac75e7", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4872.54", + "value":"-7.40" + } + },{ + "id":"cac6f521-3c79-43ca-844a-e2dbdf4d480a", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4863.87", + "value":"-8.67" + } + },{ + "id":"2c41b720-c180-4dea-acbf-2378f5187101", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Netflix" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Netflix Membership", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4858.41", + "value":"-5.46" + } + },{ + "id":"4d718642-c5d2-4959-b76b-099ef73f0565", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sainsbury's" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-10T00:00:00.000Z", + "completed":"2015-09-10T00:00:00.000Z", + "new_balance":"4824.33", + "value":"-34.08" + } + },{ + "id":"f0258d7a-9aa8-4f97-8119-f916c1740293", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"4816.93", + "value":"-7.40" + } + },{ + "id":"79ffd739-82c4-4943-8e9a-d91d1f87b759", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"4802.36", + "value":"-14.57" + } + },{ + "id":"9ebfcc39-6a56-46e9-9ca9-f2517c7003ef", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Hollywood Bolwing" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"4780.11", + "value":"-22.25" + } + },{ + "id":"9f3e3bb7-e4fa-498e-9adc-e79b0d39bbb6", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nail Art" + }, + "details":{ + "type":"Debit Card", + "description":"Nails", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4755.28", + "value":"-24.83" + } + },{ + "id":"c7c65ffb-f8d6-4b2b-8e9d-42616bff9a10", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4747.88", + "value":"-7.40" + } + },{ + "id":"bc19de22-6f42-466a-b290-8427dfd16687", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"All Staints" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4597.06", + "value":"-150.82" + } + },{ + "id":"8f644f54-a21e-483b-8248-d2730b47c38e", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Topshop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4522.81", + "value":"-74.25" + } + },{ + "id":"b76425df-902a-4ff9-ab47-7592d95c6ead", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-13T00:00:00.000Z", + "completed":"2015-09-13T00:00:00.000Z", + "new_balance":"4482.65", + "value":"-40.16" + } + },{ + "id":"f3b9e5da-7905-48fc-8369-3787c4d03978", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-14T00:00:00.000Z", + "completed":"2015-09-14T00:00:00.000Z", + "new_balance":"4475.25", + "value":"-7.40" + } + },{ + "id":"d17a509d-2043-4554-a407-be894c5c97fb", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-14T00:00:00.000Z", + "completed":"2015-09-14T00:00:00.000Z", + "new_balance":"4436.48", + "value":"-38.77" + } + },{ + "id":"d5a82482-9043-465e-b3f2-eaa1e6c57a98", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Hong Wang" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-09-16T00:00:00.000Z", + "completed":"2015-09-16T00:00:00.000Z", + "new_balance":"4386.20", + "value":"-50.28" + } + },{ + "id":"edbc49de-d8e4-4951-a067-65f0763f5c2b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Amazon" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4346.09", + "value":"-40.11" + } + },{ + "id":"8f096333-d27a-4463-9523-28df9676771f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Booking.com" + }, + "details":{ + "type":"Debit Card", + "description":"Booking.com", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"4266.30", + "value":"-79.79" + } + },{ + "id":"becd9f84-7a78-46a3-aeec-727bc2f44daa", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Easyjet" + }, + "details":{ + "type":"Debit Card", + "description":"Flights", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"4150.28", + "value":"-116.02" + } + },{ + "id":"eb58a697-44fe-4ab7-9c0d-54b96770a6d0", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Pizza Express" + }, + "details":{ + "type":"Debit Card", + "description":"Resaurant", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"4121.62", + "value":"-28.66" + } + },{ + "id":"6d5f3107-de02-4761-8b9f-2c23c8954ff1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"4114.22", + "value":"-7.40" + } + },{ + "id":"12fef7e4-57b7-426f-91d1-1d5dfb032057", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Terrace Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"4090.99", + "value":"-23.23" + } + },{ + "id":"1882d7cd-430a-4805-94f0-e9d4e2b9d988", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"4075.08", + "value":"-15.91" + } + },{ + "id":"b6288b17-dbcf-4fab-b529-a1f8b52ddde6", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"ABC Taxi" + }, + "details":{ + "type":"Debit Card", + "description":"Taxi", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"4060.32", + "value":"-14.76" + } + },{ + "id":"30f1f733-8785-4b96-87c7-402a473cf102", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"4045.55", + "value":"-14.77" + } + },{ + "id":"1d7567e0-ceb2-408d-843d-e47175a53213", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Orange Mobile" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"3995.62", + "value":"-49.93" + } + },{ + "id":"101e2c18-a8e8-4da0-bf6c-b30cd2c18c4b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Saving", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"3767.58", + "value":"-228.04" + } + },{ + "id":"36fdda56-0db8-4ba4-8fdd-a1b17b48440d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"ING Mortgage" + }, + "details":{ + "type":"Direct Debit", + "description":"Mortgage", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7744.26", + "value":"-627.15" + } + },{ + "id":"9cc68508-797a-4a55-9cc8-7a6479e4b00c", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Directline" + }, + "details":{ + "type":"Direct Debit", + "description":"Insurance", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7711.34", + "value":"-32.92" + } + },{ + "id":"1b0464b6-29ed-4187-9df3-07ea5af37bd2", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7687.24", + "value":"-24.10" + } + },{ + "id":"fac683d5-96e0-426c-8b80-7858bfb79316", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"SSE" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7635.47", + "value":"-51.77" + } + },{ + "id":"ed709ff1-bf16-46ed-b7c0-32404e803ecf", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7524.91", + "value":"-110.56" + } + },{ + "id":"930c78de-502b-4c2b-8df3-4916c4d92e89", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sky" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Sky", + "posted":"2015-07-02T00:00:00.000Z", + "completed":"2015-07-02T00:00:00.000Z", + "new_balance":"7466.31", + "value":"-58.60" + } + },{ + "id":"ac793c69-3c6e-4f51-83c2-604c093a6ff6", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"7376.23", + "value":"-90.08" + } + },{ + "id":"b3181fee-9f33-47fc-b034-e57bca1fe9bb", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Takeway King" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"7353.90", + "value":"-22.33" + } + },{ + "id":"04ee593d-5020-406c-a1db-2e08aa268448", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"7304.39", + "value":"-49.51" + } + },{ + "id":"6842199c-695a-41b5-b3db-e8478b23c5bb", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"7251.02", + "value":"-53.37" + } + },{ + "id":"f2a79b8e-9598-4dcd-988b-b448a9517a4f", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-07-06T00:00:00.000Z", + "completed":"2015-07-06T00:00:00.000Z", + "new_balance":"7247.04", + "value":"-3.98" + } + },{ + "id":"1d25a716-e928-4bbc-8c18-d234e8005813", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"7204.43", + "value":"-42.61" + } + },{ + "id":"c497d4c5-8504-4e31-9eca-f26cfeb27835", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The choach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"7174.55", + "value":"-29.88" + } + },{ + "id":"7d8b3c0f-7faf-451a-a7f3-ea3d51fb732b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Amazon" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-14T00:00:00.000Z", + "completed":"2015-07-14T00:00:00.000Z", + "new_balance":"7157.89", + "value":"-16.66" + } + },{ + "id":"f711d9c2-b458-41b4-a4ae-2a50e7e66d10", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"7151.74", + "value":"-6.15" + } + },{ + "id":"9443ef18-9447-4a71-a7ac-2c07ec64310e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-18T00:00:00.000Z", + "completed":"2015-07-18T00:00:00.000Z", + "new_balance":"7119.44", + "value":"-32.30" + } + },{ + "id":"e0b1e713-8b57-4f9e-abd5-32cc2571cdb5", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The sandwich Factory" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"7113.01", + "value":"-6.43" + } + },{ + "id":"74f556c8-a5c0-46d5-907a-8d255d5e7fb5", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash Withdrawal", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"6868.31", + "value":"-244.70" + } + },{ + "id":"617f3ae3-cde0-4467-b35e-b752448ec8f8", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Champagne Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-24T00:00:00.000Z", + "completed":"2015-07-24T00:00:00.000Z", + "new_balance":"6827.28", + "value":"-41.03" + } + },{ + "id":"51aa7fc4-e11f-4c20-94ea-96a2883157e1", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Quick Bit" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-07-24T00:00:00.000Z", + "completed":"2015-07-24T00:00:00.000Z", + "new_balance":"6817.11", + "value":"-10.17" + } + },{ + "id":"d5ec7a5e-5cbc-488d-8338-3790f8e0d6ca", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-25T00:00:00.000Z", + "completed":"2015-07-25T00:00:00.000Z", + "new_balance":"6785.64", + "value":"-31.47" + } + },{ + "id":"335abcd1-048b-4513-b145-44a91c213e19", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-25T00:00:00.000Z", + "completed":"2015-07-25T00:00:00.000Z", + "new_balance":"6730.64", + "value":"-55.00" + } + },{ + "id":"7b22e014-cb52-4558-86ec-ee5da6d299e7", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-26T00:00:00.000Z", + "completed":"2015-07-26T00:00:00.000Z", + "new_balance":"6656.86", + "value":"-73.78" + } + },{ + "id":"c6fa0179-2409-4c6b-986a-54fb35071d71", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-07-27T00:00:00.000Z", + "completed":"2015-07-27T00:00:00.000Z", + "new_balance":"6650.43", + "value":"-6.43" + } + },{ + "id":"04cc24fa-a8e7-41a7-8720-1d86d9961bd9", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid In", + "description":"Salary", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8922.08", + "value":"2271.65" + } + },{ + "id":"97104c64-bdf6-476f-a716-88e79492a990", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"ING Mortgage" + }, + "details":{ + "type":"Direct Debit", + "description":"Mortgage", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8294.93", + "value":"-627.15" + } + },{ + "id":"69b32565-d311-40da-9a56-6bd6cfe8f28d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Directline" + }, + "details":{ + "type":"Direct Debit", + "description":"Insurance", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8262.01", + "value":"-32.92" + } + },{ + "id":"bc7a71f0-9787-44fa-aa97-2d1e77744ddb", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8237.91", + "value":"-24.10" + } + },{ + "id":"3527f74e-de3c-4b9a-a1e0-531daa5a30e4", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"SSE" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8186.14", + "value":"-51.77" + } + },{ + "id":"1c174f68-a811-4348-9958-e471425c7920", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"8075.58", + "value":"-110.56" + } + },{ + "id":"f53d4f1d-3ce3-427e-9f94-f569baf60d88", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sky" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Sky", + "posted":"2015-08-02T00:00:00.000Z", + "completed":"2015-08-02T00:00:00.000Z", + "new_balance":"8016.98", + "value":"-58.60" + } + },{ + "id":"84a32d9d-24c5-424e-a7b2-d899f6c829a3", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"8004.04", + "value":"-12.94" + } + },{ + "id":"876a871b-a9f9-43ee-9d0b-145f2407fce5", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Quick Bit" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-08-04T00:00:00.000Z", + "completed":"2015-08-04T00:00:00.000Z", + "new_balance":"7989.17", + "value":"-14.87" + } + },{ + "id":"2f71edf1-2867-462f-80da-bdf459a1a650", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"7958.03", + "value":"-31.14" + } + },{ + "id":"03d8c6c7-d0cb-4f3b-8e09-a77e7ad5223e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The choach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"resturant", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"7931.53", + "value":"-26.50" + } + },{ + "id":"20040ae8-1204-4bb1-bb3c-b358bb75b721", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Showcase Cinema" + }, + "details":{ + "type":"Debit Card", + "description":"Cinema", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"7905.92", + "value":"-25.61" + } + },{ + "id":"671d07ce-a7d5-46e0-9b52-f48d11becf97", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-10T00:00:00.000Z", + "completed":"2015-08-10T00:00:00.000Z", + "new_balance":"7852.55", + "value":"-53.37" + } + },{ + "id":"7d5b2169-2040-44ad-aa2e-af0426e9f64e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"7848.44", + "value":"-4.11" + } + },{ + "id":"0cd3b708-3def-4ed8-abdb-2eba23b812dc", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Pizza Express" + }, + "details":{ + "type":"Debit Card", + "description":"restaurant", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"7827.94", + "value":"-20.50" + } + },{ + "id":"3a243ebd-015e-4264-944c-b743f1230c3b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"7799.20", + "value":"-28.74" + } + },{ + "id":"94bc6f0d-e8e4-45b5-ba67-5cacd89b6a77", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Dominos" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-08-16T00:00:00.000Z", + "completed":"2015-08-16T00:00:00.000Z", + "new_balance":"7764.42", + "value":"-34.78" + } + },{ + "id":"ae7795c1-e4fd-4082-a72a-a23d4f76d8b5", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sainsbury's" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"7719.85", + "value":"-44.57" + } + },{ + "id":"1970b872-1390-4345-808b-5120e02805da", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-08-20T00:00:00.000Z", + "completed":"2015-08-20T00:00:00.000Z", + "new_balance":"7712.45", + "value":"-7.40" + } + },{ + "id":"bc920fcd-9b1c-4b80-99f8-fb9afd18f445", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The sandwich Company" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-08-20T00:00:00.000Z", + "completed":"2015-08-20T00:00:00.000Z", + "new_balance":"7703.78", + "value":"-8.67" + } + },{ + "id":"98aba0d2-9643-44f7-8651-020491e71bdf", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-08-22T00:00:00.000Z", + "completed":"2015-08-22T00:00:00.000Z", + "new_balance":"7745.85", + "value":"42.07" + } + },{ + "id":"65655bdc-1734-401e-8b2c-e05be2b60242", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"7694.40", + "value":"-51.45" + } + },{ + "id":"850fb671-b407-4c3b-b307-be359d8c1cb8", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The choach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"7657.10", + "value":"-37.30" + } + },{ + "id":"b72f65c0-b610-4eb1-8c2b-0b6c788869cc", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"7631.66", + "value":"-25.44" + } + },{ + "id":"d2aa1cd0-3671-407f-8569-a981cfdfe1a0", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Hollywood Bowl" + }, + "details":{ + "type":"Debit Card", + "description":"Bowling", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"7605.07", + "value":"-26.59" + } + },{ + "id":"bf2dfbf0-a6c9-4ff3-a270-30d002c61b7a", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid In", + "description":"Salary", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9876.72", + "value":"2271.65" + } + },{ + "id":"402b68a5-6249-4f84-ac31-fa16fdf47ca1", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"ING Mortgage" + }, + "details":{ + "type":"Direct Debit", + "description":"Mortgage", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9249.57", + "value":"-627.15" + } + },{ + "id":"40bdfcdc-da8d-4f7b-a04a-1e524d7a3c5d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Directline" + }, + "details":{ + "type":"Direct Debit", + "description":"Insurance", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9216.65", + "value":"-32.92" + } + },{ + "id":"6cd8cecb-bfaf-419e-b25e-6e93ae06e435", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9192.55", + "value":"-24.10" + } + },{ + "id":"0d36f82b-f792-4f2b-b799-7bb12037d97f", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"SSE" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9140.78", + "value":"-51.77" + } + },{ + "id":"8cc0f345-ffc4-4692-a02b-c8c10bfc8f90", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"9030.22", + "value":"-110.56" + } + },{ + "id":"f13dab21-e762-4dc3-b33e-7275789f5947", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sky" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Sky", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"8971.62", + "value":"-58.60" + } + },{ + "id":"3e4d81ad-32b6-4a5d-b6fe-4ffff7d1ed40", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8904.40", + "value":"-67.22" + } + },{ + "id":"239dda32-b668-4cca-ad87-73e63580cc7e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Sandwich Factory" + }, + "details":{ + "type":"Debit Card", + "description":"Resaurant/Takeway", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8898.72", + "value":"-5.68" + } + },{ + "id":"cb0812d4-8f83-4b1a-84fd-f307c1c5cbd8", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8860.72", + "value":"-38.00" + } + },{ + "id":"74869ba4-cf3d-40f6-b509-971abeb53816", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Quick Bit" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurtant/Takeway", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"8850.36", + "value":"-10.36" + } + },{ + "id":"159ad2de-1101-4276-a076-79a63043261b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"8846.51", + "value":"-3.85" + } + },{ + "id":"02c3c1d7-8976-4b33-ae3c-8d4ffa4876bd", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"8804.50", + "value":"-42.01" + } + },{ + "id":"af872dbd-6da4-4e22-8046-7d96e7c832db", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-06T00:00:00.000Z", + "completed":"2015-09-06T00:00:00.000Z", + "new_balance":"8746.09", + "value":"-58.41" + } + },{ + "id":"20471b1e-3f8f-47c2-a24b-53ab5fb06ec9", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Harvey Nichols" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-06T00:00:00.000Z", + "completed":"2015-09-06T00:00:00.000Z", + "new_balance":"8678.87", + "value":"-67.22" + } + },{ + "id":"112141a9-303c-4f59-a326-e888ec0256c9", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-08T00:00:00.000Z", + "completed":"2015-09-08T00:00:00.000Z", + "new_balance":"8620.46", + "value":"-58.41" + } + },{ + "id":"bc6b81cf-bf78-45d4-aba8-2aeddc9ade03", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Amazon" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-08T00:00:00.000Z", + "completed":"2015-09-08T00:00:00.000Z", + "new_balance":"8593.23", + "value":"-27.23" + } + },{ + "id":"c8f2fedd-b129-4f99-bbd9-9a507e5f9115", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-10T00:00:00.000Z", + "completed":"2015-09-10T00:00:00.000Z", + "new_balance":"8587.91", + "value":"-5.32" + } + },{ + "id":"798b7a2a-5b17-4d03-846f-7a5eb87ac3ba", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Topman" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-10T00:00:00.000Z", + "completed":"2015-09-10T00:00:00.000Z", + "new_balance":"8507.48", + "value":"-80.43" + } + },{ + "id":"a26de2f2-c946-4cd2-97fc-affee69677c4", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"8500.05", + "value":"-7.43" + } + },{ + "id":"aa136a2f-4bd3-4706-b087-09b10ffb8150", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"8486.43", + "value":"-13.62" + } + },{ + "id":"7bf9e1bd-c6f8-4c11-8390-9234b9cbdf78", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Hollywood Bowling" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"8464.65", + "value":"-21.78" + } + },{ + "id":"bfdead68-9f5a-4b7e-95c8-79effc450b70", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-13T00:00:00.000Z", + "completed":"2015-09-13T00:00:00.000Z", + "new_balance":"8420.78", + "value":"-43.87" + } + },{ + "id":"3c6cd14d-63d3-486d-919e-4c97b340f0ae", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-16T00:00:00.000Z", + "completed":"2015-09-16T00:00:00.000Z", + "new_balance":"8415.64", + "value":"-5.14" + } + },{ + "id":"39ec4a9a-6c1f-4df4-ba52-6c1502938ab3", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Champagne Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"8320.58", + "value":"-95.06" + } + },{ + "id":"d840dfa8-e8df-4c4b-a29e-52a603be0aea", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Pizza King" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"8280.23", + "value":"-40.35" + } + },{ + "id":"ecd6b2de-239d-47cd-9cec-dfc0136b3ade", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"8275.09", + "value":"-5.14" + } + },{ + "id":"aeb1d820-9f3f-4fec-8428-2985d4a82423", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Chimichanga" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"8229.78", + "value":"-45.31" + } + },{ + "id":"f2ed38d2-6c01-4721-8e10-216e574fc931", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"8170.79", + "value":"-58.99" + } + },{ + "id":"720ceb08-9e49-4bd3-a51c-bbc951bcf61b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Liquid" + }, + "details":{ + "type":"Debit Card", + "description":"Club", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"8126.09", + "value":"-44.70" + } + },{ + "id":"5db541ed-9e1d-4438-b4c4-d190a0f75b2d", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The coach and horses" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-20T00:00:00.000Z", + "completed":"2015-09-20T00:00:00.000Z", + "new_balance":"8076.15", + "value":"-49.94" + } + },{ + "id":"d3221c41-cc26-4de8-9d44-3ece2f3048d9", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Dominos" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-09-20T00:00:00.000Z", + "completed":"2015-09-20T00:00:00.000Z", + "new_balance":"8029.14", + "value":"-47.01" + } + },{ + "id":"00f89de5-8a30-4a98-89a5-80e2eb5d5eef", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"coffee", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"8023.46", + "value":"-5.68" + } + },{ + "id":"9bdcce93-37f4-440f-9cc1-78fb263c9596", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Sandwich Factory" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"8019.02", + "value":"-4.44" + } + },{ + "id":"872bdae6-7abc-413d-b752-4d2796b14326", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"R J Dentist" + }, + "details":{ + "type":"Debit Card", + "description":"Dentist", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"7953.46", + "value":"-65.56" + } + },{ + "id":"88b2ab36-e0bb-4d02-a115-c83096aa9478", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Patels Parmacy" + }, + "details":{ + "type":"Debit Card", + "description":"Parmacy", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"7944.58", + "value":"-8.88" + } + },{ + "id":"c5e52fc4-f008-411d-a186-8a406992fade", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"7907.14", + "value":"-37.44" + } + },{ + "id":"573b5c22-39b0-478f-a043-273f3e118a0e", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"7876.14", + "value":"-31.00" + } + },{ + "id":"4309984d-bd47-4cc8-a028-0349a6945d8b", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Takeway King" + }, + "details":{ + "type":"Debit Card", + "description":"Takeway", + "posted":"2015-09-25T00:00:00.000Z", + "completed":"2015-09-25T00:00:00.000Z", + "new_balance":"7860.71", + "value":"-15.43" + } + },{ + "id":"ba57a215-bb5b-4a9e-b0e0-ae1970daf046", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Mr D Moda" + }, + "details":{ + "type":"Tranfer", + "description":"Transfer", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"7616.01", + "value":"-244.70" + } + },{ + "id":"116e01fa-6daf-48c9-ab6e-b5e960dd43a3", + "this_account":{ + "id":"2330135d-fca8-4268-838d-833074985209", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-29T00:00:00.000Z", + "completed":"2015-09-29T00:00:00.000Z", + "new_balance":"7588.25", + "value":"-27.76" + } + },{ + "id":"b2711b8d-d8ed-485d-82db-5256f2cbd620", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"7605.12", + "value":"-66.45" + } + },{ + "id":"c4ff16ec-5664-4788-98ea-9cdcb9b51c72", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"Rent", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6977.97", + "value":"-627.15" + } + },{ + "id":"90232f3c-d8d1-4913-b630-84095ad451c0", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"EON" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6924.60", + "value":"-53.37" + } + },{ + "id":"81fb1c80-1845-472f-a078-25aeb607cba6", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6891.68", + "value":"-32.92" + } + },{ + "id":"72493c1f-7e8f-4962-9da4-e9ee1cef411d", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Talk Talk" + }, + "details":{ + "type":"Direct Debit", + "description":"Telephone", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"6868.91", + "value":"-22.77" + } + },{ + "id":"f7d38e93-f3a3-4e40-84ec-68189962ed8e", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"6834.26", + "value":"-34.65" + } + },{ + "id":"1f617bc7-0346-4555-8aac-823de51ebe10", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"6782.49", + "value":"-51.77" + } + },{ + "id":"f1bf0925-23f9-43fc-be2d-b8ddfefa411b", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Poundland" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"6767.73", + "value":"-14.76" + } + },{ + "id":"565c0494-66cf-497a-8553-e52bc340d28f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Specsavers" + }, + "details":{ + "type":"Debit Card", + "description":"Optician", + "posted":"2015-07-06T00:00:00.000Z", + "completed":"2015-07-06T00:00:00.000Z", + "new_balance":"6743.63", + "value":"-24.10" + } + },{ + "id":"f66a4412-6a76-4d51-a382-6af55a970d34", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-06T00:00:00.000Z", + "completed":"2015-07-06T00:00:00.000Z", + "new_balance":"6708.05", + "value":"-35.58" + } + },{ + "id":"cbecb922-ed34-4a30-b95f-0f1db344d282", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-10T00:00:00.000Z", + "completed":"2015-07-10T00:00:00.000Z", + "new_balance":"6656.28", + "value":"-51.77" + } + },{ + "id":"31d38ca8-36b4-41ed-9fac-5068152dbc6e", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-10T00:00:00.000Z", + "completed":"2015-07-10T00:00:00.000Z", + "new_balance":"6634.40", + "value":"-21.88" + } + },{ + "id":"e896508d-3bc0-42f3-9e92-b7d09674a0b3", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"6596.82", + "value":"-37.58" + } + },{ + "id":"afa43cde-fb9c-4063-bd7d-32a18c2aaeb3", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Clarks" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"6574.05", + "value":"-22.77" + } + },{ + "id":"a45f1064-9422-42b9-aa07-4b9d9d56c5f5", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Next" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"6544.21", + "value":"-29.84" + } + },{ + "id":"82b2a2ed-8907-4929-ab1f-04cda79dfacd", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-15T00:00:00.000Z", + "completed":"2015-07-15T00:00:00.000Z", + "new_balance":"6524.65", + "value":"-19.56" + } + },{ + "id":"d2138860-f7e2-494c-9a92-449a2f219066", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"6472.88", + "value":"-51.77" + } + },{ + "id":"f2fa1731-8da3-4f95-af44-d337a32e41fa", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"6465.67", + "value":"-7.21" + } + },{ + "id":"4feb5cb4-c0b8-4976-810d-8e0342f8a439", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"6407.23", + "value":"-58.44" + } + },{ + "id":"9ef1d6cb-0182-4d86-8e90-6ec4ce0497ef", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling station", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"6377.53", + "value":"-29.70" + } + },{ + "id":"6667c0d8-4371-46ff-b63c-f029a30f041b", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Pizza hut" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-19T00:00:00.000Z", + "completed":"2015-07-19T00:00:00.000Z", + "new_balance":"6341.13", + "value":"-36.40" + } + },{ + "id":"72c50133-7e3c-41c3-ba8a-5f2e51c11068", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"6289.36", + "value":"-51.77" + } + },{ + "id":"f7e7f58b-9be9-4686-9c93-29b342d7c0b7", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-23T00:00:00.000Z", + "completed":"2015-07-23T00:00:00.000Z", + "new_balance":"6277.50", + "value":"-11.86" + } + },{ + "id":"e832358c-99b6-4f31-b263-79e3caa721ad", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-26T00:00:00.000Z", + "completed":"2015-07-26T00:00:00.000Z", + "new_balance":"6237.16", + "value":"-40.34" + } + },{ + "id":"e777093e-3de0-4d7f-abe2-75ccbec73901", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-07-27T00:00:00.000Z", + "completed":"2015-07-27T00:00:00.000Z", + "new_balance":"7247.02", + "value":"1009.86" + } + },{ + "id":"f617e340-61a6-482d-9b53-28f8253d8f5a", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"three" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile phone", + "posted":"2015-07-28T00:00:00.000Z", + "completed":"2015-07-28T00:00:00.000Z", + "new_balance":"7214.10", + "value":"-32.92" + } + },{ + "id":"0e619ca6-d7fb-4b29-9279-ba5d8ecce707", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Post Office Savings Account" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-07-30T00:00:00.000Z", + "completed":"2015-07-30T00:00:00.000Z", + "new_balance":"7186.87", + "value":"-27.23" + } + },{ + "id":"f4547d4a-892a-4a9d-b4c4-2b50e502f264", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"7120.42", + "value":"-66.45" + } + },{ + "id":"005949ad-795b-48db-b618-a991b75912b0", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"Rent", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6493.27", + "value":"-627.15" + } + },{ + "id":"bac696b4-2e73-4280-996f-c9eead165a96", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"EON" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6439.90", + "value":"-53.37" + } + },{ + "id":"ade28734-f3fb-4404-a1cd-256ca6bd28ac", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6406.98", + "value":"-32.92" + } + },{ + "id":"10739155-7e80-4570-a232-ebadb761df46", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Talk Talk" + }, + "details":{ + "type":"Direct Debit", + "description":"Telephone", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"6384.21", + "value":"-22.77" + } + },{ + "id":"ef303daa-3999-43ac-9ec1-d65d84d25914", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"6349.56", + "value":"-34.65" + } + },{ + "id":"f62d98d2-d1fa-489f-8822-8fed019c458d", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"6297.79", + "value":"-51.77" + } + },{ + "id":"5fc6c085-7f08-4bd4-8069-44e11671c139", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-13T00:00:00.000Z", + "completed":"2015-08-13T00:00:00.000Z", + "new_balance":"6273.69", + "value":"-24.10" + } + },{ + "id":"9855f22b-ec5c-44fe-8832-d2a10803f495", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Ebay" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"6244.95", + "value":"-28.74" + } + },{ + "id":"153741d5-7823-4ad6-a123-8560ca3742a1", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"The Book people" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"6234.75", + "value":"-10.20" + } + },{ + "id":"3e9f1922-7ae7-4df3-8abc-924f71d719ef", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Domino's" + }, + "details":{ + "type":"Debit Card", + "description":"Takeaway", + "posted":"2015-08-21T00:00:00.000Z", + "completed":"2015-08-21T00:00:00.000Z", + "new_balance":"6196.68", + "value":"-38.07" + } + },{ + "id":"0cd42655-e648-4fa0-9bab-145277997e3f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"6144.86", + "value":"-51.82" + } + },{ + "id":"21fd40fe-7d88-4a07-b8f2-0c669407b425", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash withdrawal", + "posted":"2015-08-23T00:00:00.000Z", + "completed":"2015-08-23T00:00:00.000Z", + "new_balance":"6093.09", + "value":"-51.77" + } + },{ + "id":"ffe427d2-b132-46ad-b10f-d358229584ec", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Argos" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-24T00:00:00.000Z", + "completed":"2015-08-24T00:00:00.000Z", + "new_balance":"6082.89", + "value":"-10.20" + } + },{ + "id":"28e3dc89-eccf-453f-b946-2dbf673c06e6", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-27T00:00:00.000Z", + "completed":"2015-08-27T00:00:00.000Z", + "new_balance":"6061.95", + "value":"-20.94" + } + },{ + "id":"48e1b751-7eb8-43bc-afeb-c5b889a15046", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-27T00:00:00.000Z", + "completed":"2015-08-27T00:00:00.000Z", + "new_balance":"6021.61", + "value":"-40.34" + } + },{ + "id":"d0c039a0-cde2-4dbc-b6bf-f43acd2af11f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-08-27T00:00:00.000Z", + "completed":"2015-08-27T00:00:00.000Z", + "new_balance":"7031.47", + "value":"1009.86" + } + },{ + "id":"7d8327c1-2462-4bc3-8238-59226da1550d", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"three" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile phone", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"6998.55", + "value":"-32.92" + } + },{ + "id":"1abc0d46-02c5-48c9-89ea-4be988cf195c", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Post Office Savings Account" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-08-30T00:00:00.000Z", + "completed":"2015-08-30T00:00:00.000Z", + "new_balance":"6971.32", + "value":"-27.23" + } + },{ + "id":"e298ad50-4e38-42fb-a848-0cf0c0b62908", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"6904.87", + "value":"-66.45" + } + },{ + "id":"c34e91a3-47d0-412b-8a25-188751b54d16", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Rent" + }, + "details":{ + "type":"Direct Debit", + "description":"Rent", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6277.72", + "value":"-627.15" + } + },{ + "id":"21cea876-5598-437c-955c-44771722f15b", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"EON" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6224.35", + "value":"-53.37" + } + },{ + "id":"4a5744ea-fc62-4cf9-bcf0-83baf8c461ee", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6191.43", + "value":"-32.92" + } + },{ + "id":"2c79ca18-b809-429f-b257-6dfc3a9fa8e4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Talk Talk" + }, + "details":{ + "type":"Direct Debit", + "description":"Telephone", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"6168.66", + "value":"-22.77" + } + },{ + "id":"05fd805f-1d45-4cac-8811-ab00df9353a1", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"6110.50", + "value":"-58.16" + } + },{ + "id":"316a4753-256f-4df5-91d2-8cad7feea2d5", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"ATM Asda" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash withdrawal", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"6058.73", + "value":"-51.77" + } + },{ + "id":"94f47bd2-f9ab-4d6b-8482-172254df5c5f", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"6044.48", + "value":"-14.25" + } + },{ + "id":"470fcd75-8334-41d5-b97b-2a69a1ce8177", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"ATM High Street" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash withdrawal", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"5992.71", + "value":"-51.77" + } + },{ + "id":"b5e14a59-3b92-4ffe-95de-aed1e2e6a4fc", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Mcdonalds" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"5982.37", + "value":"-10.34" + } + },{ + "id":"27a6bda1-ed22-4cef-976b-0bbdd307d1d2", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-13T00:00:00.000Z", + "completed":"2015-09-13T00:00:00.000Z", + "new_balance":"5922.26", + "value":"-60.11" + } + },{ + "id":"56f35dfd-4d24-451c-b5ad-1e6f038c6319", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Poundland" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-14T00:00:00.000Z", + "completed":"2015-09-14T00:00:00.000Z", + "new_balance":"5910.83", + "value":"-11.43" + } + },{ + "id":"695f2508-f808-4efb-bd49-00ee65b57564", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling station", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"5883.60", + "value":"-27.23" + } + },{ + "id":"316be52e-1c31-4dd2-84b1-92800980cc01", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Sport Direct" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"5851.59", + "value":"-32.01" + } + },{ + "id":"04dc4c9e-f28f-45d6-bcda-d219f3ad89ae", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Pizza Hut" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"5811.87", + "value":"-39.72" + } + },{ + "id":"07390e63-2339-4882-87bc-f5477922f12c", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"CO-OP" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"5787.69", + "value":"-24.18" + } + },{ + "id":"c91cc425-00c3-49bc-ba9f-de0ac8bfe308", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Paid in", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"6797.55", + "value":"1009.86" + } + },{ + "id":"e93d1d73-0401-4339-870b-602541324e92", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"6741.95", + "value":"-55.60" + } + },{ + "id":"cab07ea7-ff73-4c91-b113-45af8de782ca", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda ATM" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash withdrawal", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"6690.18", + "value":"-51.77" + } + },{ + "id":"b0751086-0a7f-4874-8af3-c7419b524fb5", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"three" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile phone", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"6657.26", + "value":"-32.92" + } + },{ + "id":"96be48c8-09c7-4a80-9206-4de560171ff4", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Post Office Savings Account" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6630.03", + "value":"-27.23" + } + },{ + "id":"21740a94-1fd5-4cff-bb27-5629fd254a87", + "this_account":{ + "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", + "bank":"obp-bank-x-gh" + }, + "counterparty":{ + "name":"Asda Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling station", + "posted":"2025-08-08T00:00:00.000Z", + "completed":"2025-08-08T00:00:00.000Z", + "new_balance":"6599.63", + "value":"-30.40" + } + },{ + "id":"ff2534c5-881c-4102-a8e0-17e3727d509a", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"3477.76", + "value":"1393.82" + } + },{ + "id":"4ec7a646-5bf9-4780-9ae3-6009c613d813", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid In", + "description":"Salary", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"5501.70", + "value":"2023.94" + } + },{ + "id":"afebaf96-5b9e-44b4-90bd-766b3ddf7f69", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Car Plan" + }, + "details":{ + "type":"Direct Debit", + "description":"Car Monthly Payment", + "posted":"2015-07-03T00:00:00.000Z", + "completed":"2015-07-03T00:00:00.000Z", + "new_balance":"5128.38", + "value":"-373.32" + } + },{ + "id":"0e58465e-5645-40d9-bbc5-86acc672d51d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"New Look" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"5075.48", + "value":"-52.90" + } + },{ + "id":"cb8d2abe-d22f-4d57-8d0f-31fecc2017fc", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Burger King" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"5066.70", + "value":"-8.78" + } + },{ + "id":"1679d777-55d8-4d95-9093-b43f613520ac", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-04T00:00:00.000Z", + "completed":"2015-07-04T00:00:00.000Z", + "new_balance":"5046.77", + "value":"-19.93" + } + },{ + "id":"8d3bc7b1-6410-497d-8a69-787839395f49", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"5035.97", + "value":"-10.80" + } + },{ + "id":"c517b676-f324-48f5-aa47-f7b66e9fae4b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-07T00:00:00.000Z", + "completed":"2015-07-07T00:00:00.000Z", + "new_balance":"5030.71", + "value":"-5.26" + } + },{ + "id":"8454982d-d706-474d-8b68-a4822e87e5a3", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Body Shop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"5011.22", + "value":"-19.49" + } + },{ + "id":"62d80fd5-9564-48d4-bdf0-632d5fa6c480", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"4993.39", + "value":"-17.83" + } + },{ + "id":"95424163-326a-45c5-bcc0-381d18d890ee", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nail Art" + }, + "details":{ + "type":"Debit Card", + "description":"Nails", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"4971.25", + "value":"-22.14" + } + },{ + "id":"ec48f3a2-3e50-4b2d-9a62-f3f2540ab31d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Terrance Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"4956.52", + "value":"-14.73" + } + },{ + "id":"319cca47-b4ae-4422-becc-df59ad153f24", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Zizzi" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-11T00:00:00.000Z", + "completed":"2015-07-11T00:00:00.000Z", + "new_balance":"4925.53", + "value":"-30.99" + } + },{ + "id":"e1947504-3ee5-44ff-898d-5f43ac76f342", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"4861.33", + "value":"-64.20" + } + },{ + "id":"175bcd89-3525-46b8-aca8-31cfb8245f74", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station ATM" + }, + "details":{ + "type":"Debit Card", + "description":"Cash Withdrawals", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"4740.74", + "value":"-120.59" + } + },{ + "id":"f74c4634-7770-4e74-a7c4-06afde828bb5", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-16T00:00:00.000Z", + "completed":"2015-07-16T00:00:00.000Z", + "new_balance":"4709.14", + "value":"-31.60" + } + },{ + "id":"1339f0ec-057b-4960-bfc1-7a2b908afd1a", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"4702.16", + "value":"-6.98" + } + },{ + "id":"fc7d4d9c-1c5f-4ac5-81bb-83fc8cb1810d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-17T00:00:00.000Z", + "completed":"2015-07-17T00:00:00.000Z", + "new_balance":"4692.02", + "value":"-10.14" + } + },{ + "id":"ea5ff1a3-1805-4e73-8bee-80bffa546854", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Salon" + }, + "details":{ + "type":"Debit Card", + "description":"Hair", + "posted":"2015-07-19T00:00:00.000Z", + "completed":"2015-07-19T00:00:00.000Z", + "new_balance":"4604.83", + "value":"-87.19" + } + },{ + "id":"54169458-cfb0-4c8a-a129-1531482bc2f7", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-21T00:00:00.000Z", + "completed":"2015-07-21T00:00:00.000Z", + "new_balance":"4580.69", + "value":"-24.14" + } + },{ + "id":"dc128d9b-dbee-485d-a601-1c436469316a", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-07-23T00:00:00.000Z", + "completed":"2015-07-23T00:00:00.000Z", + "new_balance":"4539.53", + "value":"-41.16" + } + },{ + "id":"90fce9ca-9211-450f-9b23-5fa1fe642db5", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-25T00:00:00.000Z", + "completed":"2015-07-25T00:00:00.000Z", + "new_balance":"4522.12", + "value":"-17.41" + } + },{ + "id":"d443e754-69ca-46ab-b633-59cc6dd1fd99", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-27T00:00:00.000Z", + "completed":"2015-07-27T00:00:00.000Z", + "new_balance":"4516.86", + "value":"-5.26" + } + },{ + "id":"4a2cbd2c-955f-419c-8009-689e1195d560", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Orange Mobile" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile", + "posted":"2015-07-28T00:00:00.000Z", + "completed":"2015-07-28T00:00:00.000Z", + "new_balance":"4462.66", + "value":"-54.20" + } + },{ + "id":"593be880-8f14-48f7-9016-af41086812af", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Saving", + "posted":"2015-07-28T00:00:00.000Z", + "completed":"2015-07-28T00:00:00.000Z", + "new_balance":"4225.50", + "value":"-237.16" + } + },{ + "id":"a60b50b9-4a4b-4646-a1f7-7fb032ed9c65", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"5619.32", + "value":"1393.82" + } + },{ + "id":"4062b8d3-863b-4f29-8f5b-9e4e858868f3", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Car Plan" + }, + "details":{ + "type":"Direct Debit", + "description":"Car Monthly Payment", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"5246.00", + "value":"-373.32" + } + },{ + "id":"459c390d-4309-4f45-b4d5-b671fa43e6e8", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash Withdrawals", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"5116.76", + "value":"-129.24" + } + },{ + "id":"a4fdba0c-11db-464d-ac96-505d183e763f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-04T00:00:00.000Z", + "completed":"2015-08-04T00:00:00.000Z", + "new_balance":"5101.84", + "value":"-14.92" + } + },{ + "id":"b441baf1-5ab1-42c3-aa3b-123bde37740f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"5076.49", + "value":"-25.35" + } + },{ + "id":"d57e9d38-2e6b-4baf-87f2-960efe8de142", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Zizzi" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"5045.50", + "value":"-30.99" + } + },{ + "id":"9983a87c-eb2c-4585-b801-74ee1a76491f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Netflix" + }, + "details":{ + "type":"Debit Card", + "description":"Monthly Netflix Membership", + "posted":"2015-08-07T00:00:00.000Z", + "completed":"2015-08-07T00:00:00.000Z", + "new_balance":"5038.12", + "value":"-7.38" + } + },{ + "id":"ed340169-dc4b-4139-99e5-5337e7193c2b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Topshop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"4990.29", + "value":"-47.83" + } + },{ + "id":"c76681c4-2bf9-4f46-89d3-83118a286fd1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Hand Made Burger Company" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"4937.48", + "value":"-52.81" + } + },{ + "id":"44354f0c-bf1f-4312-bd76-4356c40947f3", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High street ATM" + }, + "details":{ + "type":"Cash withdrawal", + "description":"Cash Withdrawals", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"4816.89", + "value":"-120.59" + } + },{ + "id":"6b51fdc5-1c18-41f0-bc41-a750b4a3fdcf", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"4793.74", + "value":"-23.15" + } + },{ + "id":"77b7e795-4167-4c1b-827a-d055b3089da3", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-11T00:00:00.000Z", + "completed":"2015-08-11T00:00:00.000Z", + "new_balance":"4788.48", + "value":"-5.26" + } + },{ + "id":"08c5fee4-61ca-4537-bc89-bd5d73a37003", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Terrance Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"4753.49", + "value":"-34.99" + } + },{ + "id":"7b6c843b-c88c-47be-8fe5-0788dda69b74", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"4722.50", + "value":"-30.99" + } + },{ + "id":"f9375cf4-a38f-4a8b-9e71-bcbbf64e06c9", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"4677.17", + "value":"-45.33" + } + },{ + "id":"b040daa6-885f-4b23-8153-99e49bc05603", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-17T00:00:00.000Z", + "completed":"2015-08-17T00:00:00.000Z", + "new_balance":"4670.19", + "value":"-6.98" + } + },{ + "id":"6e1dd2ea-d107-4c86-a4e7-ebea37e50b36", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Aiport Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-19T00:00:00.000Z", + "completed":"2015-08-19T00:00:00.000Z", + "new_balance":"4662.89", + "value":"-7.30" + } + },{ + "id":"d879228a-8042-44d2-b8bb-6dd3d799f366", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Orange Mobile" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"4608.69", + "value":"-54.20" + } + },{ + "id":"1aac8e76-24b1-422c-8b7d-ae5e020c2417", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Saving", + "posted":"2015-08-28T00:00:00.000Z", + "completed":"2015-08-28T00:00:00.000Z", + "new_balance":"4371.53", + "value":"-237.16" + } + },{ + "id":"bcd93119-31e3-4a90-badb-ba0e5b748499", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Salary" + }, + "details":{ + "type":"Paid in", + "description":"Salary", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"5765.35", + "value":"1393.82" + } + },{ + "id":"2edee268-bdc8-4317-909c-f819048dc719", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Car Plan" + }, + "details":{ + "type":"Direct Debit", + "description":"Car Monthly Payment", + "posted":"2015-09-03T00:00:00.000Z", + "completed":"2015-09-03T00:00:00.000Z", + "new_balance":"5392.03", + "value":"-373.32" + } + },{ + "id":"3cb12f0c-6602-4d06-b882-37e98a352039", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Zara" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5346.70", + "value":"-45.33" + } + },{ + "id":"5890d005-5225-4497-be02-2e8e2abef9d1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Boots Plc" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5320.10", + "value":"-26.60" + } + },{ + "id":"4aed082b-5370-4723-b987-ec30e1242304", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5301.09", + "value":"-19.01" + } + },{ + "id":"98482ee9-47e0-45d7-bb04-208769edb7b9", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Zizzi" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-04T00:00:00.000Z", + "completed":"2015-09-04T00:00:00.000Z", + "new_balance":"5259.93", + "value":"-41.16" + } + },{ + "id":"4606bf78-575e-4325-a0b6-65f1163fc341", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5252.95", + "value":"-6.98" + } + },{ + "id":"247ed765-3666-455a-bd3d-1122c24968f1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Ticket Master" + }, + "details":{ + "type":"Debit Card", + "description":"Tickets", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5174.14", + "value":"-78.81" + } + },{ + "id":"59147a73-1440-471d-b447-b6e0fdda8d0b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Salon" + }, + "details":{ + "type":"Debit Card", + "description":"Hair", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5075.51", + "value":"-98.63" + } + },{ + "id":"663030f6-1c70-48c5-9277-d63a3aed07f1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Texaco Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"5015.94", + "value":"-59.57" + } + },{ + "id":"64315058-832c-463d-bf35-a666318e784f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"High Street ATM" + }, + "details":{ + "type":"Cash Withdrawal", + "description":"Cash Withdrawals", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"4895.35", + "value":"-120.59" + } + },{ + "id":"6d347fdf-90bc-434d-933c-f58a05614d1c", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Showcase Cinema" + }, + "details":{ + "type":"Debit Card", + "description":"Cinema", + "posted":"2015-09-05T00:00:00.000Z", + "completed":"2015-09-05T00:00:00.000Z", + "new_balance":"4885.62", + "value":"-9.73" + } + },{ + "id":"97bf7571-cfee-459f-bdaa-f54f282ccd95", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Netflix" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Netflix Membership", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4880.20", + "value":"-5.42" + } + },{ + "id":"45663628-b9ae-4e46-a08b-730c2eeaafe7", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4873.22", + "value":"-6.98" + } + },{ + "id":"1ad01e3d-1e46-4045-84c4-d27b363d3be1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4866.99", + "value":"-6.23" + } + },{ + "id":"52bffd51-08dd-4ddf-920a-6b108f322292", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Netflix" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Netflix Membership", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"4861.57", + "value":"-5.42" + } + },{ + "id":"1527c856-76cd-4849-bba6-fc152e7d512b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sainsbury's" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-10T00:00:00.000Z", + "completed":"2015-09-10T00:00:00.000Z", + "new_balance":"4825.82", + "value":"-35.75" + } + },{ + "id":"d97772f3-f272-41be-8728-a999c5f7ebc8", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"4818.84", + "value":"-6.98" + } + },{ + "id":"fb8fc4aa-ca16-451b-b294-b43380c66f5a", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nando's" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"4807.25", + "value":"-11.59" + } + },{ + "id":"67406b53-f5a0-47f6-8e85-471132344315", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Hollywood Bolwing" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"4776.91", + "value":"-30.34" + } + },{ + "id":"df4300d1-e7ea-4b7d-9ef3-ed1491ef6bb7", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Nail Art" + }, + "details":{ + "type":"Debit Card", + "description":"Nails", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4754.77", + "value":"-22.14" + } + },{ + "id":"b660f09a-9479-4f2d-901e-121f759333c7", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4747.79", + "value":"-6.98" + } + },{ + "id":"a71b6a71-c0bd-4588-a1f7-13d92698091d", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"All Staints" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4622.78", + "value":"-125.01" + } + },{ + "id":"d762f092-844f-4818-be14-0f3fe60dec49", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Topshop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-12T00:00:00.000Z", + "completed":"2015-09-12T00:00:00.000Z", + "new_balance":"4551.25", + "value":"-71.53" + } + },{ + "id":"247d86b3-aa8f-40d4-a1b8-cd9a6730d4c9", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The White Horse" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant", + "posted":"2015-09-13T00:00:00.000Z", + "completed":"2015-09-13T00:00:00.000Z", + "new_balance":"4512.81", + "value":"-38.44" + } + },{ + "id":"6b93f9fa-1c38-4925-b6ad-1d2e1f546b80", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-14T00:00:00.000Z", + "completed":"2015-09-14T00:00:00.000Z", + "new_balance":"4505.83", + "value":"-6.98" + } + },{ + "id":"bcedb05b-b310-4791-b4b6-54a8e7627c41", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Waitrose" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-14T00:00:00.000Z", + "completed":"2015-09-14T00:00:00.000Z", + "new_balance":"4467.74", + "value":"-38.09" + } + },{ + "id":"5f91d7c4-a3dc-49c3-8dbb-812dca79ea9b", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Hong Wang" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurant/Takeway", + "posted":"2015-09-16T00:00:00.000Z", + "completed":"2015-09-16T00:00:00.000Z", + "new_balance":"4424.53", + "value":"-43.21" + } + },{ + "id":"a0b93361-1fa6-4317-b48f-d50e693aa757", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Amazon" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4392.18", + "value":"-32.35" + } + },{ + "id":"521de7b4-ffef-45e2-a74b-9033743fd135", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Booking.com" + }, + "details":{ + "type":"Debit Card", + "description":"Booking.com", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"4283.46", + "value":"-108.72" + } + },{ + "id":"77609c58-95d8-4cfb-addb-d2c6f892381e", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Easyjet" + }, + "details":{ + "type":"Debit Card", + "description":"Flights", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"4149.31", + "value":"-134.15" + } + },{ + "id":"4b7abd6b-0a7b-4de2-a6f3-28ccd7376e6e", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Pizza Express" + }, + "details":{ + "type":"Debit Card", + "description":"Resaurant", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"4115.29", + "value":"-34.02" + } + },{ + "id":"b12453ef-37d2-43a6-a51b-bb9d77bb990c", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"4108.31", + "value":"-6.98" + } + },{ + "id":"28ddaeb6-da06-4e64-a184-11db7a298058", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Terrace Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"4083.63", + "value":"-24.68" + } + },{ + "id":"f1c76615-5844-4ed4-a43e-2e0ab25aa983", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Revolution Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"4067.52", + "value":"-16.11" + } + },{ + "id":"9a4e836f-a9bf-4c83-b581-92d310d567c1", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"ABC Taxi" + }, + "details":{ + "type":"Debit Card", + "description":"Taxi", + "posted":"2015-09-26T00:00:00.000Z", + "completed":"2015-09-26T00:00:00.000Z", + "new_balance":"4054.16", + "value":"-13.36" + } + },{ + "id":"4f3551df-d622-45e5-832d-19f5568d122f", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Costa Coffee" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-27T00:00:00.000Z", + "completed":"2015-09-27T00:00:00.000Z", + "new_balance":"4039.93", + "value":"-14.23" + } + },{ + "id":"b38708b3-3261-4a43-bb97-4c36d8e46f58", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Orange Mobile" + }, + "details":{ + "type":"Direct Debit", + "description":"Mobile", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"3985.73", + "value":"-54.20" + } + },{ + "id":"12bd7753-1edd-4b7e-9677-ca8c3c2e894e", + "this_account":{ + "id":"b8ae95c5-0b62-4d74-8a31-1a9388b128aa", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Saving", + "posted":"2015-09-28T00:00:00.000Z", + "completed":"2015-09-28T00:00:00.000Z", + "new_balance":"3748.57", + "value":"-237.16" + } + },{ + "id":"ac381c33-e922-4128-b1f0-da8d78340050", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"British Gas " + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2317.59", + "value":"-120.59" + } + },{ + "id":"9c2d911b-764d-487a-93d6-1f652cfac747", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2293.28", + "value":"-24.31" + } + },{ + "id":"5afa2830-00e5-4e4a-b248-a02c856af808", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Duddingston Golf Club" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Golf membership", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2238.40", + "value":"-54.88" + } + },{ + "id":"4355080f-c89a-4cdd-b52d-56f3e84cd734", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-07-01T00:00:00.000Z", + "completed":"2015-07-01T00:00:00.000Z", + "new_balance":"2214.09", + "value":"-24.31" + } + },{ + "id":"7a132083-18f2-4505-8d59-e6aeb234c466", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"2137.65", + "value":"-76.44" + } + },{ + "id":"628ae844-c6b2-4e6a-bbc0-e892c2cbd662", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-05T00:00:00.000Z", + "completed":"2015-07-05T00:00:00.000Z", + "new_balance":"2069.12", + "value":"-68.53" + } + },{ + "id":"459eceb2-fdc7-45c7-8bdc-2207982af9ee", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-08T00:00:00.000Z", + "completed":"2015-07-08T00:00:00.000Z", + "new_balance":"2058.32", + "value":"-10.80" + } + },{ + "id":"c8bce33d-4310-4aa1-a3b6-66b75bd948d3", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Prezzo" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-07-10T00:00:00.000Z", + "completed":"2015-07-10T00:00:00.000Z", + "new_balance":"2012.85", + "value":"-45.47" + } + },{ + "id":"1e381cf5-5f5f-4efd-ba77-b9807d75c4ac", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Sainsbury's" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"1954.60", + "value":"-58.25" + } + },{ + "id":"a006842a-dfb3-487a-bec8-7c05cd27b216", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Florist" + }, + "details":{ + "type":"Debit Card", + "description":"Florist", + "posted":"2015-07-12T00:00:00.000Z", + "completed":"2015-07-12T00:00:00.000Z", + "new_balance":"1930.29", + "value":"-24.31" + } + },{ + "id":"9ecedccf-06e2-4d26-a4d1-9453bb6996a0", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"1924.93", + "value":"-5.36" + } + },{ + "id":"f5d989f8-cf2c-4fff-9214-ebc2af0a6d46", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"NCP Car Park" + }, + "details":{ + "type":"Debit Card", + "description":"Parking", + "posted":"2015-07-13T00:00:00.000Z", + "completed":"2015-07-13T00:00:00.000Z", + "new_balance":"1920.40", + "value":"-4.53" + } + },{ + "id":"508348ad-c497-49fa-9bba-b953fe28d536", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Kitchin" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-07-18T00:00:00.000Z", + "completed":"2015-07-18T00:00:00.000Z", + "new_balance":"1812.68", + "value":"-107.72" + } + },{ + "id":"df6bb658-3fe7-4925-81ab-03d45a3e758f", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"1731.67", + "value":"-81.01" + } + },{ + "id":"180a50d7-5894-42b7-9e95-db53833ae882", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-07-20T00:00:00.000Z", + "completed":"2015-07-20T00:00:00.000Z", + "new_balance":"1663.14", + "value":"-68.53" + } + },{ + "id":"94c9e3f6-d7bb-46ed-a6be-de99f55c2816", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bar Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-07-24T00:00:00.000Z", + "completed":"2015-07-24T00:00:00.000Z", + "new_balance":"1618.93", + "value":"-44.21" + } + },{ + "id":"05148330-20ee-477e-a149-2dd919e5221a", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"British Telecom" + }, + "details":{ + "type":"Direct Debit", + "description":"BT", + "posted":"2015-07-29T00:00:00.000Z", + "completed":"2015-07-29T00:00:00.000Z", + "new_balance":"1584.82", + "value":"-34.11" + } + },{ + "id":"a7b208cd-0a70-4f5a-9f6b-756c56167519", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Paid in" + }, + "details":{ + "type":"Paid in", + "description":"Income", + "posted":"2015-07-29T00:00:00.000Z", + "completed":"2015-07-29T00:00:00.000Z", + "new_balance":"3957.30", + "value":"2372.48" + } + },{ + "id":"17ca057a-f1af-4857-8294-fcf674410193", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"RBS Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-07-30T00:00:00.000Z", + "completed":"2015-07-30T00:00:00.000Z", + "new_balance":"3696.06", + "value":"-261.24" + } + },{ + "id":"5ea1d8ed-771d-4967-9f83-067a43ebf50e", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"British Gas " + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3575.47", + "value":"-120.59" + } + },{ + "id":"931ebaef-741b-4676-b2de-4d93316c5bb5", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3551.16", + "value":"-24.31" + } + },{ + "id":"55f06eb2-d60b-48a8-aacf-a2d00bceca4b", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Duddingston Golf Club" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Golf membership", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3496.28", + "value":"-54.88" + } + },{ + "id":"799b3493-a0ae-425a-9392-f4e4499efd6a", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-08-01T00:00:00.000Z", + "completed":"2015-08-01T00:00:00.000Z", + "new_balance":"3471.97", + "value":"-24.31" + } + },{ + "id":"9b055cd0-4341-4fdb-8f41-ce6e56979e7f", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The News Shop" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-03T00:00:00.000Z", + "completed":"2015-08-03T00:00:00.000Z", + "new_balance":"3465.02", + "value":"-6.95" + } + },{ + "id":"a0fa82b0-75d8-4e60-9bd9-9149e4c4f659", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-04T00:00:00.000Z", + "completed":"2015-08-04T00:00:00.000Z", + "new_balance":"3396.49", + "value":"-68.53" + } + },{ + "id":"03b1805b-5275-4012-a98a-5bedeaf43297", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3381.17", + "value":"-15.32" + } + },{ + "id":"d5113360-3b8f-47f4-9105-2513a64c57c7", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Next" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3350.17", + "value":"-31.00" + } + },{ + "id":"0b6c1036-3419-40ec-b632-7a7c16bce7ea", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"House of Fraser" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3246.61", + "value":"-103.56" + } + },{ + "id":"738ff4b0-24d5-4c5e-91c6-118879178e08", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"La Favorita Restaurant" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-08-08T00:00:00.000Z", + "completed":"2015-08-08T00:00:00.000Z", + "new_balance":"3136.52", + "value":"-110.09" + } + },{ + "id":"e761f1ee-fc5e-4897-910f-f166fded9ac4", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-09T00:00:00.000Z", + "completed":"2015-08-09T00:00:00.000Z", + "new_balance":"3102.30", + "value":"-34.22" + } + },{ + "id":"43f8c509-d933-46ce-a807-9997070b3977", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bar Bar" + }, + "details":{ + "type":"Debit Card", + "description":"Bar", + "posted":"2015-08-14T00:00:00.000Z", + "completed":"2015-08-14T00:00:00.000Z", + "new_balance":"3074.67", + "value":"-27.63" + } + },{ + "id":"ad25f208-8615-4b0a-9724-b8f95b9f135d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Prezzo" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-08-15T00:00:00.000Z", + "completed":"2015-08-15T00:00:00.000Z", + "new_balance":"3036.15", + "value":"-38.52" + } + },{ + "id":"f85f9901-78d0-4a25-81d8-add2d17d49f3", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-08-19T00:00:00.000Z", + "completed":"2015-08-19T00:00:00.000Z", + "new_balance":"3030.89", + "value":"-5.26" + } + },{ + "id":"6376fcdb-d302-43a6-8a5a-2a1f05509407", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-08-22T00:00:00.000Z", + "completed":"2015-08-22T00:00:00.000Z", + "new_balance":"2982.97", + "value":"-47.92" + } + },{ + "id":"6c563eac-1b75-4eea-b5b1-110623aa898d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-08-26T00:00:00.000Z", + "completed":"2015-08-26T00:00:00.000Z", + "new_balance":"2914.44", + "value":"-68.53" + } + },{ + "id":"359445d6-492d-4eea-bc4e-8aaa2003e0b7", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"British Telecom" + }, + "details":{ + "type":"Direct Debit", + "description":"BT", + "posted":"2015-08-29T00:00:00.000Z", + "completed":"2015-08-29T00:00:00.000Z", + "new_balance":"2880.33", + "value":"-34.11" + } + },{ + "id":"5191d5c4-1851-4169-b152-80a5f07af8b1", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Paid in" + }, + "details":{ + "type":"Paid in", + "description":"Income", + "posted":"2015-08-29T00:00:00.000Z", + "completed":"2015-08-29T00:00:00.000Z", + "new_balance":"5252.81", + "value":"2372.48" + } + },{ + "id":"36fbafcf-409f-4b4a-ae87-26fe1e4bdc40", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"RBS Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-08-30T00:00:00.000Z", + "completed":"2015-08-30T00:00:00.000Z", + "new_balance":"4991.57", + "value":"-261.24" + } + },{ + "id":"e149772c-74b0-42c6-afe1-e9e91ea68bc3", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"British Gas" + }, + "details":{ + "type":"Direct Debit", + "description":"Gas/Elec", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4870.98", + "value":"-120.59" + } + },{ + "id":"86124761-8e2d-46fd-b51d-b3b0f69d2bf9", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Seven Trent Water" + }, + "details":{ + "type":"Direct Debit", + "description":"Water", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4846.67", + "value":"-24.31" + } + },{ + "id":"162a922e-6caa-48fb-8b21-f9a4de84d448", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Duddingston Golf Club" + }, + "details":{ + "type":"Direct Debit", + "description":"Monthly Golf membership", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4791.79", + "value":"-54.88" + } + },{ + "id":"a4aecb48-27ee-4d35-b3ac-6f2e7f44f23e", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Council Tax" + }, + "details":{ + "type":"Direct Debit", + "description":"Council Tax", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"4767.48", + "value":"-24.31" + } + },{ + "id":"85897b9a-17b1-440c-a2db-767cabdc721b", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"4699.28", + "value":"-68.20" + } + },{ + "id":"73756f16-7608-4260-a5f7-d49df7a2160d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Shell Filling Station" + }, + "details":{ + "type":"Debit Card", + "description":"Filling Station", + "posted":"2015-09-09T00:00:00.000Z", + "completed":"2015-09-09T00:00:00.000Z", + "new_balance":"4640.17", + "value":"-59.11" + } + },{ + "id":"59293fee-1c23-4c87-ab44-daed4d62e72f", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"The Cellar Door" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"4570.32", + "value":"-69.85" + } + },{ + "id":"7bd27711-650e-43b4-ba93-013e50f6292d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Debit Card", + "description":"Coffee", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4565.62", + "value":"-4.70" + } + },{ + "id":"9e510750-a650-4308-bb6a-874c1a5bd77e", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Debenhams" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4466.99", + "value":"-98.63" + } + },{ + "id":"2c653345-9543-47fc-9d3a-7de07992a65a", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Next" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4415.82", + "value":"-51.17" + } + },{ + "id":"c6d7dbb2-4331-49f1-99fe-0fac337d48e0", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Pizza Express" + }, + "details":{ + "type":"Debit Card", + "description":"Restaurants", + "posted":"2015-09-18T00:00:00.000Z", + "completed":"2015-09-18T00:00:00.000Z", + "new_balance":"4380.76", + "value":"-35.06" + } + },{ + "id":"549ff0a9-a496-4497-acf3-019df417e7f4", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Marks and Spencers" + }, + "details":{ + "type":"Debit Card", + "description":"Shopping", + "posted":"2015-09-22T00:00:00.000Z", + "completed":"2015-09-22T00:00:00.000Z", + "new_balance":"4302.50", + "value":"-78.26" + } + },{ + "id":"73879ef3-a868-4d63-b7b8-e22a35058b1d", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"British Telecom" + }, + "details":{ + "type":"Direct Debit", + "description":"BT", + "posted":"2015-09-29T00:00:00.000Z", + "completed":"2015-09-29T00:00:00.000Z", + "new_balance":"4268.39", + "value":"-34.11" + } + },{ + "id":"1abcea29-198a-459f-b3b6-9d21b8d46c46", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Paid in" + }, + "details":{ + "type":"Paid in", + "description":"Income", + "posted":"2015-09-29T00:00:00.000Z", + "completed":"2015-09-29T00:00:00.000Z", + "new_balance":"6640.87", + "value":"2372.48" + } + },{ + "id":"4963fa86-faa9-4790-b23a-6dfd01be0543", + "this_account":{ + "id":"e0ec24be-5ab1-4760-9189-ad280228c134", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"RBS Savings" + }, + "details":{ + "type":"Standing Order", + "description":"Savings", + "posted":"2015-09-30T00:00:00.000Z", + "completed":"2015-09-30T00:00:00.000Z", + "new_balance":"6379.63", + "value":"-261.24" + } + },{ + "id":"d25e1b4d-f039-4a4c-9326-6be86c613001", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Credit Card", + "description":"Bar", + "posted":"2010-09-27T00:00:00.000Z", + "completed":"2010-09-27T00:00:00.000Z", + "new_balance":"8397.68", + "value":"-13.36" + } + },{ + "id":"bbc96ba6-e6cf-4d74-9830-8b76d1b9dad6", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-01T00:00:00.000Z", + "completed":"2015-09-01T00:00:00.000Z", + "new_balance":"8352.35", + "value":"-45.33" + } + },{ + "id":"e2542689-b38c-44dd-9652-bda8b87ea43a", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco Filling Station" + }, + "details":{ + "type":"Credit Card", + "description":"Filling Station", + "posted":"2015-09-02T00:00:00.000Z", + "completed":"2015-09-02T00:00:00.000Z", + "new_balance":"8299.33", + "value":"-53.02" + } + },{ + "id":"a555bf67-faaa-4cb2-8ebc-9c119d799446", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Bambo Bar" + }, + "details":{ + "type":"Credit Card", + "description":"Bar", + "posted":"2015-09-07T00:00:00.000Z", + "completed":"2015-09-07T00:00:00.000Z", + "new_balance":"8240.63", + "value":"-58.70" + } + },{ + "id":"40c01613-1928-4bb1-9f7a-1ccd5e3784ff", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Dominos" + }, + "details":{ + "type":"Credit Card", + "description":"Takeway", + "posted":"2015-09-11T00:00:00.000Z", + "completed":"2015-09-11T00:00:00.000Z", + "new_balance":"8231.60", + "value":"-9.03" + } + },{ + "id":"1134ae57-a35c-4bbc-a6f8-a78f13cc7394", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Credit Card", + "description":"coffee", + "posted":"2015-09-15T00:00:00.000Z", + "completed":"2015-09-15T00:00:00.000Z", + "new_balance":"8227.23", + "value":"-4.37" + } + },{ + "id":"18fa0804-f213-467e-bcf6-80e8aa5c90bb", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Prezzo" + }, + "details":{ + "type":"Credit Card", + "description":"resturant", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"8192.29", + "value":"-34.94" + } + },{ + "id":"467f9e4c-b431-4991-8950-7055ad576b40", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"H Samuals" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"7931.05", + "value":"-261.24" + } + },{ + "id":"c86f126e-0d51-4a4c-a3fa-b04246667141", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Zara" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-17T00:00:00.000Z", + "completed":"2015-09-17T00:00:00.000Z", + "new_balance":"7801.81", + "value":"-129.24" + } + },{ + "id":"bb7a0c00-f837-4011-8bac-c98680ae6526", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Starbucks" + }, + "details":{ + "type":"Credit Card", + "description":"coffee", + "posted":"2015-09-19T00:00:00.000Z", + "completed":"2015-09-19T00:00:00.000Z", + "new_balance":"7797.44", + "value":"-4.37" + } + },{ + "id":"7d52ca8b-2171-4ea2-af34-a1a889b6febb", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Showcase Cinema" + }, + "details":{ + "type":"Credit Card", + "description":"Cinema", + "posted":"2015-09-21T00:00:00.000Z", + "completed":"2015-09-21T00:00:00.000Z", + "new_balance":"7778.77", + "value":"-18.67" + } + },{ + "id":"4776b669-cf79-409e-89d7-9bfa2da78438", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"Tesco" + }, + "details":{ + "type":"Credit Card", + "description":"Shopping", + "posted":"2015-09-23T00:00:00.000Z", + "completed":"2015-09-23T00:00:00.000Z", + "new_balance":"7733.44", + "value":"-45.33" + } + },{ + "id":"ffd0f38d-e76e-483a-969b-89a156058116", + "this_account":{ + "id":"4e1c02a1-2f9f-4587-9433-80b949c5a346", + "bank":"obp-bank-y-gh" + }, + "counterparty":{ + "name":"NCP Car Park" + }, + "details":{ + "type":"Credit Card", + "description":"Parking", + "posted":"2015-09-24T00:00:00.000Z", + "completed":"2015-09-24T00:00:00.000Z", + "new_balance":"7724.41", + "value":"-9.03" + } + }], + "branches":[{ + "id":"1234-d640", + "bank_id":"obp-bank-x-gh", + "name":"Head Office", + "address":{ + "line_1":"PO Box 2", + "line_2":"", + "line_3":"", + "city":"5 St. Jane Street", + "county":"", + "state":"", + "post_code":"BB5 1LY", + "country_code":"GB" + }, + "location":{ + "latitude":54.752158, + "longitude":-2.366534 + }, + "meta":{ + "license":{ + "id":"pddl", + "name":"Open Data Commons Public Domain Dedication and License (PDDL)" + } + }, + "lobby":{ + "hours":"Monday: 09:00 - 16:30
Tuesday: 09:00 - 16:30
Wednesday: 09:30 - 16:30
Thursday: 09:00 - 16:30
Friday: 09:00 - 16:30
Saturday: 09:00 - 12:30
Sunday: Closed" + }, + "drive_up":{ + "hours":"" + } + },{ + "id":"7658-23d2", + "bank_id":"obp-bank-x-gh", + "name":"Some Place", + "address":{ + "line_1":"20 London Road", + "line_2":"", + "line_3":"", + "city":"Alderley Edge", + "county":"", + "state":"", + "post_code":"SK9 7EF", + "country_code":"GB" + }, + "location":{ + "latitude":54.300288, + "longitude":-2.236626 + }, + "meta":{ + "license":{ + "id":"pddl", + "name":"Open Data Commons Public Domain Dedication and License (PDDL)" + } + }, + "lobby":{ + "hours":"Monday: 09:30 - 15:00
Tuesday: 09:30 - 15:00
Wednesday: 09:30 - 15:00
Thursday: 10:00 - 15:00
Friday: 09:30 - 15:00
Saturday: Closed
Sunday: Closed" + }, + "drive_up":{ + "hours":"" + } + },{ + "id":"18484-8e5b", + "bank_id":"obp-bank-y-gh", + "name":"Other Place", + "address":{ + "line_1":"Warrington Street", + "line_2":"", + "line_3":"", + "city":"Ashton Under Lyne", + "county":"", + "state":"", + "post_code":"OL6 6JL", + "country_code":"GB" + }, + "location":{ + "latitude":54.488144, + "longitude":-2.093318 + }, + "meta":{ + "license":{ + "id":"pddl", + "name":"Open Data Commons Public Domain Dedication and License (PDDL)" + } + }, + "lobby":{ + "hours":"Monday: 09:00 - 17:00
Tuesday: 09:00 - 17:00
Wednesday: 09:30 - 17:00
Thursday: 09:00 - 17:00
Friday: 09:00 - 17:00
Saturday: 09:00 - 13:00
Sunday: Closed" + }, + "drive_up":{ + "hours":"" + } + }], + "atms":[{ + "id":"24242-646b", + "bank_id":"obp-bank-x-gh", + "name":"Here place", + "address":{ + "line_1":"12 NORTH-WEST CIRCUS PLACE", + "line_2":"", + "line_3":"", + "city":"EDINBURGH", + "county":"GBR", + "state":"", + "post_code":"EH3 6SX", + "country_code":"GB" + }, + "location":{ + "latitude":55.967547, + "longitude":-3.216868 + }, + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "id":"234-6b91", + "bank_id":"obp-bank-y-gh", + "name":"Somewhere there", + "address":{ + "line_1":"MARKET PLACE", + "line_2":"", + "line_3":"", + "city":"EYEMOUTH", + "county":"GBR", + "state":"", + "post_code":"TD14 5HE", + "country_code":"GB" + }, + "location":{ + "latitude":55.882244, + "longitude":-2.099188 + }, + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "id":"78987-ae25", + "bank_id":"obp-bank-y-gh", + "name":"Somewhere here", + "address":{ + "line_1":"540A LANARK ROAD", + "line_2":"", + "line_3":"", + "city":"EDINBURGH", + "county":"GBR", + "state":"", + "post_code":"EH14 5EL", + "country_code":"GB" + }, + "location":{ + "latitude":55.912969, + "longitude":-3.38662 + }, + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + }], + "products":[{ + "bank_id":"obp-bank-x-gh", + "code":"M35-3857", + "name":"OFFSET FLEXIBLE MORTGAGE", + "category":"Mortgage", + "family":"Mortgage", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"H44-3e4e", + "name":"SUPER GOLD", + "category":"Account", + "family":"Service", + "super_family":"Service", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"J87-6157", + "name":"Premier Mastercard", + "category":"Credit Card", + "family":"Credit Card", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"JF-8491", + "name":"Loan Quote", + "category":"Loan", + "family":"Loan", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"M55-3a87", + "name":"Generic MTA/Current Account", + "category":"Current Accounts", + "family":"Service", + "super_family":"Service", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"KJH-8361", + "name":"Generic Savings", + "category":"Savings", + "family":"Credit", + "super_family":"Credit", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"FDFD-6ac4", + "name":"Generic Overdraft Product", + "category":"Overdraft", + "family":"Loan", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"DFD-b0cf", + "name":"Generic MTA Upgrades Product", + "category":"MTA Upgrades", + "family":"MTA Upgrades", + "super_family":"MTA Upgrades", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-x-gh", + "code":"DS-0267", + "name":"Generic Credit Card Product", + "category":"Credit Card", + "family":"Credit Card", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"FDS-85a6", + "name":"OFFSET FLEXIBLE MORTGAGE", + "category":"Mortgage", + "family":"Mortgage", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"F45-c3dd", + "name":"RESERVE ACCOUNT", + "category":"Account", + "family":"Service", + "super_family":"Service", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"FDF-6827", + "name":"Red Mastercard", + "category":"Credit Card", + "family":"Credit Card", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"AA-f370", + "name":"Loan Quote", + "category":"Loan", + "family":"Loan", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"SD4-6c17", + "name":"Generic MTA/Current Account", + "category":"Current Accounts", + "family":"Account", + "super_family":"Service", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"FD2-6ee5", + "name":"Generic Savings", + "category":"Savings", + "family":"Credit", + "super_family":"Credit", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"DF4-a232", + "name":"Generic Overdraft Product", + "category":"Overdraft", + "family":"Loan", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"FD5-ee8b", + "name":"Generic MTA Upgrades Product", + "category":"MTA Upgrades", + "family":"MTA Upgrades", + "super_family":"MTA Upgrades", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"DFD-0a61", + "name":"Generic Credit Card Product", + "category":"Credit Cards", + "family":"Credit Card", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"DF4-7d76", + "name":"Generic MTA Upgrades Product", + "category":"MTA Upgrades", + "family":"MTA Upgrades", + "super_family":"MTA Upgrades", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + },{ + "bank_id":"obp-bank-y-gh", + "code":"FD1-8a42", + "name":"Generic Credit Card Product", + "category":"Credit Cards", + "family":"Credit Card", + "super_family":"Lending", + "more_info_url":"www.example.com", + "meta":{ + "license":{ + "id":"copyrightrbs2015", + "name":"Copyright 2015 Royal Bank of Scotland" + } + } + }], + "crm_events":[{ + "id":"42f69509-3de6-402b-aa1e-c9e919828824", + "bank_id":"obp-bank-x-gh", + "customer":{ + "name":"Dennis X.0.GH", + "number":"bank-x92320234" + }, + "category":"TEST CATEGORY", + "detail":"TEST DETAIL", + "channel":"Test Channel", + "actual_date":"2015-02-14T00:00:00.000Z" + },{ + "id":"2481f7cb-d368-4cc6-9e67-0a78127bade2", + "bank_id":"obp-bank-y-gh", + "customer":{ + "name":"Dennis Y.9.GH", + "number":"bank-y121217504" + }, + "category":"TEST CATEGORY", + "detail":"TEST DETAIL", + "channel":"Test Channel", + "actual_date":"2015-02-14T00:00:00.000Z" + }] + } From 4535197330d84b12898685de581afb6446f1f244 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 8 Nov 2015 11:28:14 +0000 Subject: [PATCH 263/702] Adding updateAccountLabel from 1.2.1 to 1.4.0 --- src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala index b852284e9..7859946ac 100644 --- a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala @@ -18,6 +18,7 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable { Implementations1_2_1.privateAccountsAtOneBank, Implementations1_2_1.publicAccountsAtOneBank, Implementations1_2_1.accountById, + Implementations1_2_1.updateAccountLabel, Implementations1_2_1.getViewsForBankAccount, Implementations1_2_1.createViewForBankAccount, Implementations1_2_1.updateViewForBankAccount, From b95636f3fd0a675cbadb78af4b8d1fd41ef8d921 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 8 Nov 2015 14:50:14 +0000 Subject: [PATCH 264/702] Adding webui_sdks_url in Props and using it on index --- src/main/resources/props/sample.props.template | 5 +++++ src/main/scala/code/snippet/WebUI.scala | 11 +++++++++++ src/main/webapp/index.html | 12 +++++------- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index d60d113ec..422493217 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -111,6 +111,11 @@ webui_api_explorer_url = http://sofi.openbankproject.com/api-explorer webui_api_documentation_url = https://github.com/OpenBankProject/OBP-API/wiki +# Link for SDKs +webui_sdks_url = https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SDKS + + + ## For partner logos and links webui_main_partners=[\ {"logoUrl":"http://www.example.com/images/logo.png", "homePageUrl":"http://www.example.com", "altText":"Example 1"},\ diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index c21fec415..15c1b0a8e 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -63,10 +63,21 @@ class WebUI extends Loggable{ ".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", "")) } + // Points to the documentation. Probably a sandbox specific link is good. def apiDocumentationLink: CssSel = { ".api-documentation-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_documentation_url", "https://github.com/OpenBankProject/OBP-API/wiki")) } + // For example customers and credentials + // This relies on the page for sandbox documentation having an anchor called example-customer-logins + def exampleSandboxCredentialsLink: CssSel = { + ".example_sandbox_credentials_link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_documentation_url", "") + "#example-customer-logins") + } + + // For link to OAuth Client SDKs + def sdksLink: CssSel = { + ".sdks_link a [href]" #> scala.xml.Unparsed(Props.get("webui_sdks_url", "")) + } def mainStyleSheet: CssSel = { "#main_style_sheet [href]" #> scala.xml.Unparsed(Props.get("webui_main_style_sheet", "/media/css/website.css")) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index a94a51264..bf8ddeae7 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -37,19 +37,17 @@ Berlin 13359, Germany

Get Started

    -
  1. Register / Login as a developer here
  2. -
  3. Get one or more developer keys here
  4. -
  5. Read the documentation here
  6. -
  7. Explore the API here
  8. -
+
  • Explore the API here (use example customer logins found here)
  • +
  • Get API keys and SDKs to build your Apps.
  • +


    From f12bd6c3ecfeb13bff786a554a053dda75f66458 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 8 Nov 2015 14:50:46 +0000 Subject: [PATCH 265/702] Adding back 1.2.1 payments to 1.4.0 --- src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala index 7859946ac..73bc3d2c2 100644 --- a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala @@ -78,7 +78,7 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable { Implementations1_2_1.updateWhereTagForViewOnTransaction, Implementations1_2_1.deleteWhereTagForViewOnTransaction, Implementations1_2_1.getCounterpartyForTransaction, - //note: removed 1.2.1 makePayment + Implementations1_2_1.makePayment, // Back for a while // New in 1.3.0 Implementations1_3_0.getCards, From c24ba654010161676143dec1ff6d1befa9fa5b7c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 9 Nov 2015 10:46:34 +0000 Subject: [PATCH 266/702] tweaking index --- src/main/webapp/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index bf8ddeae7..c17a3d394 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -45,8 +45,8 @@ Berlin 13359, Germany

      -
    1. Explore the API here (use example customer logins found here)
    2. -
    3. Get API keys and SDKs to build your Apps.
    4. +
    5. Explore the API here using example customer logins here.
    6. +
    7. Get API keys and SDKs.

    From c613acab1598233f2ea67e7845944a7fa49070e0 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 10 Nov 2015 07:21:51 +0000 Subject: [PATCH 267/702] Tweak to index.html --- src/main/webapp/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index c17a3d394..7b07f9eff 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -45,9 +45,9 @@ Berlin 13359, Germany

      -
    1. Explore the API here using example customer logins here.
    2. +
    3. Explore the API here using the example customer logins here.
    4. Get API keys and SDKs.
    5. -
    +


    From 65669137e21fbea856b966ed0cabc097ad90f937 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 10 Nov 2015 07:49:38 +0000 Subject: [PATCH 268/702] Adding default for webui_sdks_url --- src/main/scala/code/snippet/WebUI.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index 15c1b0a8e..a392f9969 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -76,7 +76,7 @@ class WebUI extends Loggable{ // For link to OAuth Client SDKs def sdksLink: CssSel = { - ".sdks_link a [href]" #> scala.xml.Unparsed(Props.get("webui_sdks_url", "")) + ".sdks_link a [href]" #> scala.xml.Unparsed(Props.get("webui_sdks_url", "https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SDKS")) } def mainStyleSheet: CssSel = { From a6546239e1fff2a5d134b91ff87218b680bfffb8 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 13 Nov 2015 06:22:44 +0100 Subject: [PATCH 269/702] comment on getBankAccountType --- src/main/scala/code/bankconnectors/LocalMappedConnector.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index d60b1595c..3520fb5b9 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -106,6 +106,7 @@ object LocalMappedConnector extends Connector with Loggable { } } + // Question: Why is this called getBankAccountType? Why not getBankAccount? override def getBankAccountType(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = { MappedBankAccount.find( By(MappedBankAccount.bank, bankId.value), From e31a8e587cc1350ff205b1f8bd6396e589f4d6d7 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 13 Nov 2015 06:24:34 +0100 Subject: [PATCH 270/702] Tweaking resource docs summary for get accounts calls --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index d13952320..e581d08b4 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -280,7 +280,7 @@ trait APIMethods121 { "privateAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/private", - "Get private accounts for a single bank.", + "Get accounts (private) for a single bank.", """Returns the list of private (non-public) accounts at BANK_ID that the user has access to. |For each account the API returns the ID and the available views. | @@ -308,7 +308,7 @@ trait APIMethods121 { "publicAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/public", - "Get public accounts for a single bank.", + "Get accounts (public) for a single bank.", """Returns a list of the public accounts at BANK_ID. For each account the API returns the ID and the available views. | |Authentication via OAuth is not required.""", From 99de7524e131e3009339e6e03a01750907014062 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Tue, 24 Nov 2015 11:49:05 +0100 Subject: [PATCH 271/702] Initial commit --- .../scala/code/api/v1_4_0/APIMethods140.scala | 13 ++- .../api/v1_4_0/SwaggerJSONFactory1_4_0.scala | 86 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 360ee7203..3929d0ae7 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -334,8 +334,19 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } } } + case "resource-docs" :: "swagger" :: Nil JsonGet _ => { + user => { + for { + rd <- getResourceDocsList + } yield { + // Format the data as json + val json = SwaggerJSONFactory1_4_0.createSwaggerResourceDoc(rd) + // Return + successJsonResponse(Extraction.decompose(json)) + } + } + } } - /* transaction requests (new payments since 1.4.0) */ diff --git a/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala new file mode 100644 index 000000000..e60d238d1 --- /dev/null +++ b/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala @@ -0,0 +1,86 @@ +package code.api.v1_4_0 + +import code.api.util.APIUtil.ResourceDoc +import net.liftweb.util.Props +import net.liftweb.json._ + + +import scala.collection.immutable.ListMap + +object SwaggerJSONFactory1_4_0 { + + case class ContactJson( + name: String, + url: String + ) + + case class InfoJson( + title: String, + description: String, + contact: ContactJson, + version: String + ) + + case class ResponseObjectSchemaJson(`$ref`: String) + case class ResponseObjectJson(description: Option[String], schema: Option[ResponseObjectSchemaJson]) + + case class MethodJson(tags: List[String], + summary: String, + operationId: String, + responses: Map[String, ResponseObjectJson]) + + case class PathsJson(get: MethodJson) + + case class MessageJson(`type`: String) + + case class CodeJson(`type`: String, format: String) + + case class PropertiesJson(code: CodeJson, message: MessageJson) + + case class ErrorDefinitionJson(`type`: String, required: List[String], properties: PropertiesJson) + + case class DefinitionsJson(Error: ErrorDefinitionJson) + + case class SwaggerResourceDoc(swagger: String, + info: InfoJson, + host: String, + basePath: String, + schemes: List[String], + paths: Map[String, Map[String, MethodJson]], + definitions: DefinitionsJson + ) + + def createSwaggerResourceDoc(resourceDocList: List[ResourceDoc]): SwaggerResourceDoc = { + implicit val formats = DefaultFormats + val contact = ContactJson("OBP", "https://openbankproject.com/") + val appVersion = "v1.4.0" + val title = "Open Bank Project API Explorer" + val description = "Use the API in the context of your login. Use the select boxes below to auto populate url parameters such as BANK_ID, ACCOUNT_ID and VIEW_ID. Some options and calls require login. " + val info = InfoJson(title, description, contact, appVersion) + val host = Props.get("hostname", "unknown host").replaceFirst("http://", "") + val basePath = "/obp/" + appVersion + val schemas = List("http") + val paths: ListMap[String, Map[String, MethodJson]] = resourceDocList.groupBy(x => x.requestUrl).toSeq.sortBy(x => x._1).map { mrd => + val methods: Map[String, MethodJson] = mrd._2.map(rd => + (rd.requestVerb, + MethodJson( + List(s"${rd.apiVersion.toString}"), + rd.summary, + s"${rd.apiVersion.toString}-${rd.apiFunction.toString}", + Map("200" -> ResponseObjectJson(Some("Success") , None), "400" -> ResponseObjectJson(Some("Error"), Some(ResponseObjectSchemaJson("#/definitions/Error")))))) + ).toMap + (mrd._1, methods.toSeq.sortBy(m => m._1).toMap) + }(collection.breakOut) + + val `type` = "object" + val required = List("code", "message") + val code = CodeJson("integer", "int32") + val message = MessageJson("string") + val properties = PropertiesJson(code, message) + val errorDef = ErrorDefinitionJson(`type`, required, properties) + val defs = DefinitionsJson(errorDef) + + SwaggerResourceDoc("2.0", info, host, basePath, schemas, paths, defs) + } + +} \ No newline at end of file From 853f53f44fa6b21558e89fc312b4d52e9d73c489 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 24 Nov 2015 22:05:25 +0100 Subject: [PATCH 272/702] Better error message in case of missing importer_secret in Props file --- src/main/scala/code/management/ImporterAPI.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/management/ImporterAPI.scala b/src/main/scala/code/management/ImporterAPI.scala index 7c6489c63..5d6cf90c2 100644 --- a/src/main/scala/code/management/ImporterAPI.scala +++ b/src/main/scala/code/management/ImporterAPI.scala @@ -184,7 +184,7 @@ object ImporterAPI extends RestHelper with Loggable { savetransactions else errorJsonResponse("wrong secret", 401) - case _ => errorJsonResponse("importer_secret not set") + case _ => errorJsonResponse("importer_secret not set on the server.") } } case _ => errorJsonResponse("secret missing") From c8303f59c89d7175a2d8d7b1b6f49172faab39eb Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 24 Nov 2015 22:53:18 +0100 Subject: [PATCH 273/702] Splitting resourceDocs OBP and Swagger into two separate functions. Adding Swagger to resource docs. --- .../scala/code/api/v1_4_0/APIMethods140.scala | 27 ++++++++++++++++--- .../scala/code/api/v1_4_0/OBPAPI1_4_0.scala | 3 ++- .../api/v1_4_0/SwaggerJSONFactory1_4_0.scala | 4 +-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 3929d0ae7..4a1b660be 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -298,13 +298,13 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ resourceDocs += ResourceDoc( apiVersion, - "getResourceDocs", + "getResourceDocsObp", "GET", "/resource-docs/obp", "Get Resource Documentation in OBP format.", """Returns documentation about the RESTful resources on this server including example body for POST or PUT requests. | Thus the OBP API Explorer (and other apps) can display and work with the API documentation. - | In the future this information will be used to create Swagger and RAML files. + | In the future this information will be used to create Swagger (WIP) and RAML files. |
      |
    • operation_id is concatenation of version and function and should be unque (the aim of this is to allow links to code)
    • |
    • version references the version that the API call is defined in.
    • @@ -321,7 +321,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ // Provides resource documents so that API Explorer (or other apps) can display API documentation // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple various of markdown. - lazy val getResourceDocs : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + lazy val getResourceDocsObp : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "obp" :: Nil JsonGet _ => { user => { for { @@ -334,6 +334,24 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } } } + } + + + resourceDocs += ResourceDoc( + apiVersion, + "getResourceDocsSwagger", + "GET", + "/resource-docs/swagger", + "Get Resource Documentation in Swagger format. Work In Progress!", + """Returns documentation about the RESTful resources on this server in Swagger format. + | Currently this is incomplete. + """, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil + ) + + lazy val getResourceDocsSwagger : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "swagger" :: Nil JsonGet _ => { user => { for { @@ -347,6 +365,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } } } + + + /* transaction requests (new payments since 1.4.0) */ diff --git a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala index 73bc3d2c2..19155efbd 100644 --- a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala @@ -93,7 +93,8 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable { Implementations1_4_0.getAtms, Implementations1_4_0.getProducts, Implementations1_4_0.getCrmEvents, - Implementations1_4_0.getResourceDocs, + Implementations1_4_0.getResourceDocsObp, + Implementations1_4_0.getResourceDocsSwagger, Implementations1_4_0.createTransactionRequest, Implementations1_4_0.getTransactionRequests, Implementations1_4_0.getTransactionRequestTypes, diff --git a/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala index e60d238d1..c354c5176 100644 --- a/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala @@ -54,8 +54,8 @@ object SwaggerJSONFactory1_4_0 { implicit val formats = DefaultFormats val contact = ContactJson("OBP", "https://openbankproject.com/") val appVersion = "v1.4.0" - val title = "Open Bank Project API Explorer" - val description = "Use the API in the context of your login. Use the select boxes below to auto populate url parameters such as BANK_ID, ACCOUNT_ID and VIEW_ID. Some options and calls require login. " + val title = "Open Bank Project API" + val description = "An open source API for banks." val info = InfoJson(title, description, contact, appVersion) val host = Props.get("hostname", "unknown host").replaceFirst("http://", "") val basePath = "/obp/" + appVersion From 45ced7c605224c254d4d63479bcacb3a11739936 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 21 Dec 2015 12:30:21 +0100 Subject: [PATCH 274/702] OAuth from Apache to AGPL --- .../code/snippet/OAuthAuthorisation.scala | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/main/scala/code/snippet/OAuthAuthorisation.scala b/src/main/scala/code/snippet/OAuthAuthorisation.scala index 53322fe58..3ad1fcbb9 100644 --- a/src/main/scala/code/snippet/OAuthAuthorisation.scala +++ b/src/main/scala/code/snippet/OAuthAuthorisation.scala @@ -1,32 +1,37 @@ /** -Open Bank Project +Open Bank Project - API +Copyright (C) 2011 - 2015, TESOBE Ltd. -Copyright 2011,2012 TESOBE / Music Pictures Ltd. +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. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +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. -http://www.apache.org/licenses/LICENSE-2.0 +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +This product includes software developed at +TESOBE (http://www.tesobe.com/) +by +Simon Redfern : simon AT tesobe DOT com +Stefan Bethge : stefan AT tesobe DOT com +Everett Sochowski : everett AT tesobe DOT com +Ayoub Benali: ayoub AT tesobe DOT com - Open Bank Project (http://www.openbankproject.com) - Copyright 2011,2012 TESOBE / Music Pictures Ltd - This product includes software developed at - TESOBE (http://www.tesobe.com/) +TESOBE Ltd. +Osloer Str. 16/17 +Berlin 13359, Germany +Email: contact@tesobe.com + +*/ - by - Simon Redfern : simon AT tesobe DOT com - Everett Sochowski: everett AT tesobe DOT com - Ayoub Benali : ayoub AT tesobe Dot com - */ package code.snippet import code.util.Helper @@ -168,7 +173,7 @@ object OAuthAuthorisation { } - //looks for expired tokens and nonces and delete them + //looks for expired tokens and nonces and deletes them def dataBaseCleaner: Unit = { import net.liftweb.util.Schedule import net.liftweb.mapper.By_< From c742dc0a6ab72fe19d6c294a1248fe9f7e87c537 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 23 Dec 2015 06:57:47 +0100 Subject: [PATCH 275/702] Removing reference to Apache license for OAuth files in README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index f17eecf22..7457a0119 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ Please refer to the [wiki](https://github.com/OpenBankProject/OBP-API/wiki) to s ## LICENSE -This project is dual licensed under the AGPL V3 (see NOTICE) and a commercial license from TESOBE -Some files (OAuth related) are licensed under the Apache 2 license. +This project is dual licensed under the AGPL V3 (see NOTICE) and commercial licenses from TESOBE Ltd. ## SETUP From 3646f171fbf6c115a5417d84e4c32cde965ade4c Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 29 Dec 2015 10:33:40 +0100 Subject: [PATCH 276/702] Handpicked merge with kafka-connector branch --- .../resources/props/sample.props.template | 10 + .../props/test.default.props.template | 10 + .../scala/code/bankconnectors/Connector.scala | 2 + .../code/bankconnectors/KafkaHelper.scala | 192 +++++ .../bankconnectors/KafkaMappedConnector.scala | 802 ++++++++++++++++++ 5 files changed, 1016 insertions(+) create mode 100644 src/main/scala/code/bankconnectors/KafkaHelper.scala create mode 100644 src/main/scala/code/bankconnectors/KafkaMappedConnector.scala diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 422493217..d91ffaf07 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -9,8 +9,18 @@ #which data connector to use connector=mapped #connector=mongodb +#connector=kafka #connector=... +#if using kafka connector, set zookeeper host +#defaults to "localhost:2181" if not set +#kafka.zookeeper_host=123.45.67.89:2181 + +#if using kafka connector, the following is mandatory +#kafka.group_id=1 +#kafka.request_topic=Request +#kafka.response_topic=Response + #you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url #db.driver=org.postgresql.Driver #db.driver=org.h2.Driver diff --git a/src/main/resources/props/test.default.props.template b/src/main/resources/props/test.default.props.template index b9544e0da..1e4eead3f 100644 --- a/src/main/resources/props/test.default.props.template +++ b/src/main/resources/props/test.default.props.template @@ -8,8 +8,18 @@ #connector=mongodb #connector=rest +#connector=kafka connector=mapped +#if using kafka connector, set zookeeper host +#defaults to "localhost:2181" if not set +#kafka.zookeeper_host=123.45.67.89:2181 + +#if using kafka connector, the following is mandatory +#kafka.group_id=1 +#kafka.request_topic=Request +#kafka.response_topic=Response + #this is needed for oauth to work. it's important to access the api over this url, e.g. # if this is 127.0.0.1 don't use localhost to access it. # (this needs to be an URL) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 07af4cdbb..cfdc3e897 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -22,6 +22,7 @@ import scala.util.Random So we can switch between different sources of resources e.g. - Mapper ORM for connecting to RDBMS (via JDBC) https://www.assembla.com/wiki/show/liftweb/Mapper - MongoDB +- KafkaMQ etc. Note: We also have individual providers for resources like Branches and Products. @@ -39,6 +40,7 @@ object Connector extends SimpleInjector { Props.get("connector").openOrThrowException("no connector set") match { case "mapped" => LocalMappedConnector case "mongodb" => LocalConnector + case "kafka" => KafkaMappedConnector } } diff --git a/src/main/scala/code/bankconnectors/KafkaHelper.scala b/src/main/scala/code/bankconnectors/KafkaHelper.scala new file mode 100644 index 000000000..29c9d6983 --- /dev/null +++ b/src/main/scala/code/bankconnectors/KafkaHelper.scala @@ -0,0 +1,192 @@ +package code.bankconnectors + +/* +Open Bank Project - API +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see http://www.gnu.org/licenses/. + +Email: contact@tesobe.com +TESOBE / Music Pictures Ltd +Osloerstrasse 16/17 +Berlin 13359, Germany +*/ + +import java.util.{Properties, UUID} + +import scala.concurrent.ops._ +import scala.concurrent.duration._ + +import net.liftweb.util.Props + +import kafka.utils.{ZkUtils, ZKStringSerializer} +import org.I0Itec.zkclient.ZkClient +import kafka.consumer.Consumer +import kafka.consumer._ +import kafka.consumer.KafkaStream +import kafka.message._ +import kafka.producer.{KeyedMessage, Producer, ProducerConfig} + + +object ZooKeeperUtils { + // gets brokers tracked by zookeeper + def getBrokers(zookeeper:String): List[String] = { + //connect to zookeeper client + val zkClient = new ZkClient(zookeeper, 30000, 30000, ZKStringSerializer) + // get list of all available kafka brokers + val brokers = for {broker <- ZkUtils.getAllBrokersInCluster(zkClient)} yield {broker.host +":"+ broker.port} + // close zookeeper client before returning the result + zkClient.close() + brokers.toList + } + // gets all topics tracked by zookeeper + def getTopics(zookeeper:String): List[String] = { + //connect to zookeeper client + val zkClient = new ZkClient(zookeeper, 30000, 30000, ZKStringSerializer) + // get list of all available kafka topics + val res = ZkUtils.getAllTopics(zkClient).toList + zkClient.close() + res + } +} + + +class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")openOr("localhost:2181"), + val groupId: String = Props.get("kafka.group_id").openOrThrowException("no kafka.group_id set"), + val topic: String = Props.get("kafka.response_topic").openOrThrowException("no kafka.response_topic set"), + val delay: Long = 0) { + + val config = createConsumerConfig(zookeeper, groupId) + val consumer = Consumer.create(config) + + def shutdown() = { + if (consumer != null) + consumer.shutdown() + } + def createConsumerConfig(zookeeper: String, groupId: String): ConsumerConfig = { + val props = new Properties() + props.put("zookeeper.connect", zookeeper) + props.put("group.id", groupId) + props.put("auto.offset.reset", "smallest") + props.put("zookeeper.session.timeout.ms", "6000") + props.put("zookeeper.connection.timeout.ms", "6000") + props.put("session.timeout.ms", "6000"); + props.put("zookeeper.sync.time.ms", "1000") + props.put("consumer.timeout.ms", "6000") + props.put("auto.commit.enable", "true"); + props.put("auto.commit.interval.ms", "1000") + val config = new ConsumerConfig(props) + config + } + def getResponse(reqId: String): List[Map[String, String]] = { + // create single stream for topic + val topicCountMap = Map(topic -> 1) + val consumerMap = consumer.createMessageStreams(topicCountMap) + val streams = consumerMap.get(topic).get + // process streams + for (stream <- streams) { + val it = stream.iterator() + try { + // wait for message + while (it.hasNext()) { + val mIt = it.next() + val msg = new String(mIt.message(), "UTF8") + val key = new String(mIt.key(), "UTF8") + // check if the id matches + if (key == reqId) { + // disconnect from kafka + shutdown() + // split result if it contains multiple answers + val msgList = msg.split("\\},\\{") + // match '"":""', with possible space after colon + val p = """"([a-zA-Z0-9_-]*?)":"(.*?)"""".r + val r = (for( m <- msgList) yield (for( p(k, v) <- p.findAllIn(m) ) yield (k -> v)).toMap[String, String]).toList + return r; + } + } + } + catch { + case e:kafka.consumer.ConsumerTimeoutException => println("Exception: " + e.toString()) + // disconnect from kafka + shutdown() + return List(Map("error" -> "timeout")) + } + } + // disconnect from kafka + shutdown() + return List(Map("" -> "")) + } +} + +import ZooKeeperUtils._ + +case class KafkaProducer( + topic: String = Props.get("kafka.request_topic").openOrThrowException("no kafka.request_topic set"), + brokerList: String = getBrokers(Props.get("kafka.zookeeper_host")openOr("localhost:2181")).mkString(","), + clientId: String = UUID.randomUUID().toString, + synchronously: Boolean = true, + compress: Boolean = true, + batchSize: Integer = 200, + messageSendMaxRetries: Integer = 3, + requestRequiredAcks: Integer = -1 + ) { + + // determine compression codec + val codec = if (compress) DefaultCompressionCodec.codec else NoCompressionCodec.codec + + // configure producer + val props = new Properties() + props.put("compression.codec", codec.toString) + props.put("producer.type", if (synchronously) "sync" else "async") + props.put("metadata.broker.list", brokerList) + props.put("batch.num.messages", batchSize.toString) + props.put("message.send.max.retries", messageSendMaxRetries.toString) + props.put("request.required.acks", requestRequiredAcks.toString) + props.put("client.id", clientId.toString) + props.put("session.timeout.ms", "4000"); + + // create producer + val producer = new Producer[AnyRef, AnyRef](new ProducerConfig(props)) + + // create keyed message since we will use the key as id for matching response to a request + def kafkaMesssage(key: Array[Byte], message: Array[Byte], partition: Array[Byte]): KeyedMessage[AnyRef, AnyRef] = { + if (partition == null) { + // no partiton specified + new KeyedMessage(topic, key, message) + } else { + // specific partition + new KeyedMessage(topic, key, partition, message) + } + } + + def send(key: String, request: String, arguments: Map[String, String], partition: String = null): Unit = { + // create string from named map of arguments + val args = (for ( (k,v) <- arguments ) yield { s""""$k":"$v",""" }).mkString.replaceAll(",$", "") + // create message using request and arguments strings + val message = s"$request:{$args}" + // translate strings to utf8 before sending to kafka + send(key.getBytes("UTF8"), message.getBytes("UTF8"), if (partition == null) null else partition.getBytes("UTF8")) + } + + def send(key: Array[Byte], message: Array[Byte], partition: Array[Byte]): Unit = { + try { + // actually send the message to kafka + producer.send(kafkaMesssage(key, message, partition)) + } catch { + case e: Exception => + e.printStackTrace() + } + } +} + diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala new file mode 100644 index 000000000..7c489bd6c --- /dev/null +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -0,0 +1,802 @@ +package code.bankconnectors + +import java.util.{Calendar, Date, UUID, TimeZone, Locale, Properties} +import java.text.{SimpleDateFormat, DateFormat} + +import code.metadata.comments.MappedComment +import code.metadata.counterparties.Counterparties +import code.metadata.narrative.MappedNarrative +import code.metadata.tags.MappedTag +import code.metadata.transactionimages.MappedTransactionImage +import code.metadata.wheretags.MappedWhereTag +import code.model.dataAccess.ViewImpl +import code.model.dataAccess.ViewPrivileges + +import code.model._ +import code.model.dataAccess.{UpdatesRequestSender, MappedBankAccount, MappedAccountHolder, MappedBank} +import code.tesobe.CashTransaction +import code.management.ImporterAPI.ImporterTransaction +import code.transactionrequests.{TransactionRequests, MappedTransactionRequest} +import code.transactionrequests.TransactionRequests.{TransactionRequestChallenge, TransactionRequest, TransactionRequestBody} +import code.util.Helper +import com.tesobe.model.UpdateBankAccount +import net.liftweb.common.{Loggable, Full, Box, Failure} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers._ +import net.liftweb.util.{False, Props} + +import scala.concurrent.ops._ + +object KafkaMappedConnector extends Connector with Loggable { + + type AccountType = MappedBankAccount + + //gets a particular bank handled by this connector + override def getBank(bankId: BankId): Box[Bank] = + getMappedBank(bankId) + + private def getMappedBank(bankId: BankId): Box[MappedBank] = + MappedBank.find(By(MappedBank.permalink, bankId.value)) + + //gets banks handled by this connector + override def getBanks: List[Bank] = + MappedBank.findAll + +/* + //gets banks handled by this connector + override def getBanks: List[Bank] = { + // Generate random uuid to be used as request-respose match id + val reqId: String = UUID.randomUUID().toString + // Create Kafka producer + val producer: KafkaProducer = new KafkaProducer() + // Create empty argument list + val argList: Map[String, String] = Map() + // Send request to Kafka, marked with reqId + // so we can fetch the corresponding response + producer.send(reqId, "getBanks", argList, "1") + // Request sent, now we wait for response with the same reqId + val consumer = new KafkaConsumer() + val rList = consumer.getResponse(reqId) + // Loop through list of responses and create entry for each + val res = { for ( r <- rList ) yield { + MappedBank.create + .permalink(r.getOrElse("BankId", "")) + .shortBankName(r.getOrElse("shortBankName", "")) + .fullBankName(r.getOrElse("fullBankName", "")) + .logoURL(r.getOrElse("logoURL", "")) + .websiteURL(r.getOrElse("websiteURL", "")) + } + } + // Return list of results + res + } + + // Gets bank identified by bankId + override def getBank(bankId: code.model.BankId): Box[Bank] = { + // Generate random uuid to be used as request-respose match id + val reqId: String = UUID.randomUUID().toString + // Create Kafka producer + val producer: KafkaProducer = new KafkaProducer() + // Create argument list + val argList = Map( "bankId" -> bankId.toString ) + // Send request to Kafka, marked with reqId + // so we can fetch the corresponding response + producer.send(reqId, "getBank", argList, "1") + // Request sent, now we wait for response with the same reqId + val consumer = new KafkaConsumer() + // Create entry only for the first item on returned list + val r = consumer.getResponse(reqId).head + val res = MappedBank.create + .permalink(r.getOrElse("bankId", "")) + .shortBankName(r.getOrElse("shortBankName", "")) + .fullBankName(r.getOrElse("fullBankName", "")) + .logoURL(r.getOrElse("logoURL", "")) + .websiteURL(r.getOrElse("websiteURL", "")) + // Return result + Full(res) + } +*/ + + // Gets transaction identified by bankid, accountid and transactionId + def getTransaction(bankId: BankId, accountID: AccountId, transactionId: TransactionId): Box[Transaction] = { + + updateAccountTransactions(bankId, accountID) + + // Generate random uuid to be used as request-respose match id + val reqId: String = UUID.randomUUID().toString + + // Create Kafka producer, using list of brokers from Zookeeper + val producer: KafkaProducer = new KafkaProducer() + // Send request to Kafka, marked with reqId + // so we can fetch the corresponding response + val argList = Map( "bankId" -> bankId.toString, + "accountId" -> accountID.toString, + "transactionId" -> transactionId.toString ) + producer.send(reqId, "getTransaction", argList, "1") + + // Request sent, now we wait for response with the same reqId + val consumer = new KafkaConsumer() + // Create entry only for the first item on returned list + val r = consumer.getResponse(reqId).head + + // If empty result from Kafka return empty data + if (r.getOrElse("accountId", "") == "") { + val res = MappedTransaction.find( + By(MappedTransaction.transactionId, "EMPTY")).flatMap(_.toTransaction) + return res + } + + // helper for creating otherbankaccount + def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { + new OtherBankAccount( + id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), + label = r.getOrElse("label", ""), + nationalIdentifier = r.getOrElse("nationalIdentifier ", ""), + swift_bic = Some(r.getOrElse("swift_bic", "")), //TODO: need to add this to the json/model? + iban = Some(r.getOrElse("iban", "")), + number = r.getOrElse("number", ""), + bankName = r.getOrElse("bankName", ""), + kind = r.getOrElse("accountType", ""), + originalPartyBankId = new BankId(r.getOrElse("bankId", "")), + originalPartyAccountId = new AccountId(r.getOrElse("accountId", "")), + alreadyFoundMetadata = alreadyFoundMetadata + ) + } + //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) + val dummyOtherBankAccount = createOtherBankAccount(None) + //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object + //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init + val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata)) + + Full( + new Transaction( + TransactionId(r.getOrElse("accountId", "")).value, // uuid:String + TransactionId(r.getOrElse("accountId", "")), // id:TransactionId + getBankAccount(BankId(r.getOrElse("bankId", "")), AccountId(r.getOrElse("accountId", ""))).openOr(null), // thisAccount:BankAccount + otherAccount, // otherAccount:OtherBankAccount + r.getOrElse("transactionType", ""), // transactionType:String + BigDecimal(r.getOrElse("amount", "0.0")), // val amount:BigDecimal + r.getOrElse("currency", ""), // currency:String + Some(r.getOrElse("description", "")), // description:Option[String] + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.getOrElse("startDate", "1970-01-01T00:00:00.000Z")), // startDate:Date + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.getOrElse("finishDate", "1970-01-01T00:00:00.000Z")), // finishDate:Date + BigDecimal(r.getOrElse("balance", "0.0")) // balance:BigDecimal + )) + } + + + override def getTransactions(bankId: BankId, accountID: AccountId, queryParams: OBPQueryParam*): Box[List[Transaction]] = { + val limit = queryParams.collect { case OBPLimit(value) => MaxRows[MappedTransaction](value) }.headOption + val offset = queryParams.collect { case OBPOffset(value) => StartAt[MappedTransaction](value) }.headOption + val fromDate = queryParams.collect { case OBPFromDate(date) => By_>=(MappedTransaction.tFinishDate, date) }.headOption + val toDate = queryParams.collect { case OBPToDate(date) => By_<=(MappedTransaction.tFinishDate, date) }.headOption + val ordering = queryParams.collect { + //we don't care about the intended sort field and only sort on finish date for now + case OBPOrdering(_, direction) => + direction match { + case OBPAscending => OrderBy(MappedTransaction.tFinishDate, Ascending) + case OBPDescending => OrderBy(MappedTransaction.tFinishDate, Descending) + } + } + + val optionalParams : Seq[QueryParam[MappedTransaction]] = Seq(limit.toSeq, offset.toSeq, fromDate.toSeq, toDate.toSeq, ordering.toSeq).flatten + val mapperParams = Seq(By(MappedTransaction.bank, bankId.value), By(MappedTransaction.account, accountID.value)) ++ optionalParams + + //val mappedTransactions = MappedTransaction.findAll(mapperParams: _*) + + //////////////////////////////////////////////////////////// + //// Populate Transactions with data from from Kafka sandbox + // + // Generate random uuid to be used as request-response match id + val reqId: String = UUID.randomUUID().toString + // Create Kafka producer, using list of brokers from Zookeeper + val producer: KafkaProducer = new KafkaProducer() + // Send request to Kafka, marked with reqId + // so we can fetch the corresponding response + val argList = Map( "bankId" -> bankId.toString, + "accountId" -> accountID.toString, + "queryParams" -> queryParams.toString ) + producer.send(reqId, "getTransactions", argList, "1") + // Request sent, now we wait for response with the same reqId + val consumer = new KafkaConsumer() + // Create entry only for the first item on returned list + val rList = consumer.getResponse(reqId) + // Return blank if empty + if (rList(0).getOrElse("accountId", "") == "") { + return Full(List()) + } + // Populate fields and generate result + val res = { for ( r <- rList ) yield { + // helper for creating otherbankaccount + def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { + new OtherBankAccount( + id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), + label = r.getOrElse("label", ""), + nationalIdentifier = r.getOrElse("nationalIdentifier ", ""), + swift_bic = Some(r.getOrElse("swift_bic", "")), //TODO: need to add this to the json/model + iban = Some(r.getOrElse("iban", "")), + number = r.getOrElse("number", ""), + bankName = r.getOrElse("bankName", ""), + kind = r.getOrElse("accountType", ""), + originalPartyBankId = new BankId(r.getOrElse("bankId", "")), + originalPartyAccountId = new AccountId(r.getOrElse("accountId", "")), + alreadyFoundMetadata = alreadyFoundMetadata + ) + } + //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) + val dummyOtherBankAccount = createOtherBankAccount(None) + //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object + //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init + val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata)) + new Transaction( + TransactionId(r.getOrElse("transactionId", "")).value, // uuid:String + TransactionId(r.getOrElse("transactionId", "")), // id:TransactionId + getBankAccount(BankId(r.getOrElse("bankId", "")), AccountId(r.getOrElse("accountId", ""))).openOr(null), // thisAccount:BankAccount + otherAccount, // otherAccount:OtherBankAccount + r.getOrElse("transactionType", ""), // transactionType:String + BigDecimal(r.getOrElse("amount", "0.0")), // val amount:BigDecimal + r.getOrElse("currency", ""), // currency:String + Some(r.getOrElse("description", "")), // description:Option[String] + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.getOrElse("startDate", "1970-01-01T00:00:00.000Z")), // startDate:Date + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.getOrElse("finishDate", "1970-01-01T00:00:00.000Z")), // finishDate:Date + BigDecimal(r.getOrElse("balance", "0.0")) // balance:BigDecimal + ) + } + } + + updateAccountTransactions(bankId, accountID) + + return Full(res) + } + + + /** + * + * refreshes transactions via hbci if the transaction info is sourced from hbci + * + * Checks if the last update of the account was made more than one hour ago. + * if it is the case we put a message in the message queue to ask for + * transactions updates + * + * It will be used each time we fetch transactions from the DB. But the test + * is performed in a different thread. + */ + private def updateAccountTransactions(bankId : BankId, accountID : AccountId) = { + + for { + bank <- getMappedBank(bankId) + account <- getBankAccountType(bankId, accountID) + } { + spawn{ + val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) + val outDatedTransactions = Box!!account.accountLastUpdate.get match { + case Full(l) => now after time(l.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1))) + case _ => true + } + if(outDatedTransactions && useMessageQueue) { + UpdatesRequestSender.sendMsg(UpdateBankAccount(account.accountNumber.get, bank.national_identifier.get)) + } + } + } + } + + override def getBankAccountType(bankId: BankId, accountID: AccountId): Box[MappedBankAccount] = { + MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, accountID.value)) + } + + //gets the users who are the legal owners/holders of the account + override def getAccountHolders(bankId: BankId, accountID: AccountId): Set[User] = + MappedAccountHolder.findAll( + By(MappedAccountHolder.accountBankPermalink, bankId.value), + By(MappedAccountHolder.accountPermalink, accountID.value)).map(accHolder => accHolder.user.obj).flatten.toSet + + + def getOtherBankAccount(thisAccountBankId : BankId, thisAccountId : AccountId, metadata : OtherBankAccountMetadata) : Box[OtherBankAccount] = { + //because we don't have a db backed model for OtherBankAccounts, we need to construct it from an + //OtherBankAccountMetadata and a transaction + for { //find a transaction with this counterparty + t <- MappedTransaction.find( + By(MappedTransaction.bank, thisAccountBankId.value), + By(MappedTransaction.account, thisAccountId.value), + By(MappedTransaction.counterpartyAccountHolder, metadata.getHolder), + By(MappedTransaction.counterpartyAccountNumber, metadata.getAccountNumber)) + } yield { + new OtherBankAccount( + //counterparty id is defined to be the id of its metadata as we don't actually have an id for the counterparty itself + id = metadata.metadataId, + label = metadata.getHolder, + nationalIdentifier = t.counterpartyNationalId.get, + swift_bic = None, + iban = t.getCounterpartyIban(), + number = metadata.getAccountNumber, + bankName = t.counterpartyBankName.get, + kind = t.counterpartyAccountKind.get, + originalPartyBankId = thisAccountBankId, + originalPartyAccountId = thisAccountId, + alreadyFoundMetadata = Some(metadata) + ) + } + } + + // Get all counterparties related to an account + override def getOtherBankAccounts(bankId: BankId, accountID: AccountId): List[OtherBankAccount] = + Counterparties.counterparties.vend.getMetadatas(bankId, accountID).flatMap(getOtherBankAccount(bankId, accountID, _)) + + // Get one counterparty related to a bank account + override def getOtherBankAccount(bankId: BankId, accountID: AccountId, otherAccountID: String): Box[OtherBankAccount] = + // Get the metadata and pass it to getOtherBankAccount to construct the other account. + Counterparties.counterparties.vend.getMetadata(bankId, accountID, otherAccountID).flatMap(getOtherBankAccount(bankId, accountID, _)) + + override def getPhysicalCards(user: User): Set[PhysicalCard] = + Set.empty + + override def getPhysicalCardsForBank(bankId: BankId, user: User): Set[PhysicalCard] = + Set.empty + + + override def makePaymentImpl(fromAccount: MappedBankAccount, toAccount: MappedBankAccount, amt: BigDecimal, description : String): Box[TransactionId] = { + val fromTransAmt = -amt //from account balance should decrease + val toTransAmt = amt //to account balance should increase + + //we need to save a copy of this payment as a transaction in each of the accounts involved, with opposite amounts + val sentTransactionId = saveTransaction(fromAccount, toAccount, fromTransAmt, description) + saveTransaction(toAccount, fromAccount, toTransAmt, description) + + sentTransactionId + } + + /** + * Saves a transaction with amount @amt and counterparty @counterparty for account @account. Returns the id + * of the saved transaction. + */ + private def saveTransaction(account : MappedBankAccount, counterparty : BankAccount, amt : BigDecimal, description : String) : Box[TransactionId] = { + + val transactionTime = now + val currency = account.currency + + + //update the balance of the account for which a transaction is being created + val newAccountBalance : Long = account.accountBalance.get + Helper.convertToSmallestCurrencyUnits(amt, account.currency) + account.accountBalance(newAccountBalance).save() + + val mappedTransaction = MappedTransaction.create + .bank(account.bankId.value) + .account(account.accountId.value) + .transactionType("sandbox-payment") + .amount(Helper.convertToSmallestCurrencyUnits(amt, currency)) + .newAccountBalance(newAccountBalance) + .currency(currency) + .tStartDate(transactionTime) + .tFinishDate(transactionTime) + .description(description) + .counterpartyAccountHolder(counterparty.accountHolder) + .counterpartyAccountNumber(counterparty.number) + .counterpartyAccountKind(counterparty.accountType) + .counterpartyBankName(counterparty.bankName) + .counterpartyIban(counterparty.iban.getOrElse("")) + .counterpartyNationalId(counterparty.nationalIdentifier).saveMe + + Full(mappedTransaction.theTransactionId) + } + + /* + Transaction Requests + */ + + override def createTransactionRequestImpl(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType, + account : BankAccount, counterparty : BankAccount, body: TransactionRequestBody, + status: String) : Box[TransactionRequest] = { + val mappedTransactionRequest = MappedTransactionRequest.create + .mTransactionRequestId(transactionRequestId.value) + .mType(transactionRequestType.value) + .mFrom_BankId(account.bankId.value) + .mFrom_AccountId(account.accountId.value) + .mBody_To_BankId(counterparty.bankId.value) + .mBody_To_AccountId(counterparty.accountId.value) + .mBody_Value_Currency(body.value.currency) + .mBody_Value_Amount(body.value.amount) + .mBody_Description(body.description) + .mStatus(status) + .mStartDate(now) + .mEndDate(now).saveMe + Full(mappedTransactionRequest).flatMap(_.toTransactionRequest) + } + + override def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId): Box[Boolean] = { + val mappedTransactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) + mappedTransactionRequest match { + case Full(tr: MappedTransactionRequest) => Full(tr.mTransactionIDs(transactionId.value).save) + case _ => Failure("Couldn't find transaction request ${transactionRequestId}") + } + } + + override def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge): Box[Boolean] = { + val mappedTransactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) + mappedTransactionRequest match { + case Full(tr: MappedTransactionRequest) => Full{ + tr.mChallenge_Id(challenge.id) + tr.mChallenge_AllowedAttempts(challenge.allowed_attempts) + tr.mChallenge_ChallengeType(challenge.challenge_type).save + } + case _ => Failure(s"Couldn't find transaction request ${transactionRequestId} to set transactionId") + } + } + + override def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String): Box[Boolean] = { + val mappedTransactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) + mappedTransactionRequest match { + case Full(tr: MappedTransactionRequest) => Full(tr.mStatus(status).save) + case _ => Failure(s"Couldn't find transaction request ${transactionRequestId} to set status") + } + } + + + override def getTransactionRequestsImpl(fromAccount : BankAccount) : Box[List[TransactionRequest]] = { + val transactionRequests = MappedTransactionRequest.findAll(By(MappedTransactionRequest.mFrom_AccountId, fromAccount.accountId.value), + By(MappedTransactionRequest.mFrom_BankId, fromAccount.bankId.value)) + + Full(transactionRequests.flatMap(_.toTransactionRequest)) + } + + override def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] = { + val transactionRequest = MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value)) + transactionRequest.flatMap(_.toTransactionRequest) + } + + + override def getTransactionRequestTypesImpl(fromAccount : BankAccount) : Box[List[TransactionRequestType]] = { + //TODO: write logic / data access + Full(List(TransactionRequestType("SANDBOX_TAN"))) + } + + /* + Bank account creation + */ + + //creates a bank account (if it doesn't exist) and creates a bank (if it doesn't exist) + //again assume national identifier is unique + override def createBankAndAccount(bankName: String, bankNationalIdentifier: String, accountNumber: String, accountHolderName: String): (Bank, BankAccount) = { + //don't require and exact match on the name, just the identifier + val bank = MappedBank.find(By(MappedBank.national_identifier, bankNationalIdentifier)) match { + case Full(b) => + logger.info(s"bank with id ${b.bankId} and national identifier ${b.nationalIdentifier} found") + b + case _ => + logger.info(s"creating bank with national identifier $bankNationalIdentifier") + //TODO: need to handle the case where generatePermalink returns a permalink that is already used for another bank + MappedBank.create + .permalink(Helper.generatePermalink(bankName)) + .fullBankName(bankName) + .shortBankName(bankName) + .national_identifier(bankNationalIdentifier) + .saveMe() + } + + //TODO: pass in currency as a parameter? + val account = createAccountIfNotExisting(bank.bankId, AccountId(UUID.randomUUID().toString), accountNumber, "EUR", 0L, accountHolderName) + + (bank, account) + } + + //for sandbox use -> allows us to check if we can generate a new test account with the given number + override def accountExists(bankId: BankId, accountNumber: String): Boolean = { + MappedBankAccount.count( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.accountNumber, accountNumber)) > 0 + } + + //remove an account and associated transactions + override def removeAccount(bankId: BankId, accountID: AccountId) : Boolean = { + //delete comments on transactions of this account + val commentsDeleted = MappedComment.bulkDelete_!!( + By(MappedComment.bank, bankId.value), + By(MappedComment.account, accountID.value) + ) + + //delete narratives on transactions of this account + val narrativesDeleted = MappedNarrative.bulkDelete_!!( + By(MappedNarrative.bank, bankId.value), + By(MappedNarrative.account, accountID.value) + ) + + //delete narratives on transactions of this account + val tagsDeleted = MappedTag.bulkDelete_!!( + By(MappedTag.bank, bankId.value), + By(MappedTag.account, accountID.value) + ) + + //delete WhereTags on transactions of this account + val whereTagsDeleted = MappedWhereTag.bulkDelete_!!( + By(MappedWhereTag.bank, bankId.value), + By(MappedWhereTag.account, accountID.value) + ) + + //delete transaction images on transactions of this account + val transactionImagesDeleted = MappedTransactionImage.bulkDelete_!!( + By(MappedTransactionImage.bank, bankId.value), + By(MappedTransactionImage.account, accountID.value) + ) + + //delete transactions of account + val transactionsDeleted = MappedTransaction.bulkDelete_!!( + By(MappedTransaction.bank, bankId.value), + By(MappedTransaction.account, accountID.value) + ) + + //remove view privileges (get views first) + val views = ViewImpl.findAll( + By(ViewImpl.bankPermalink, bankId.value), + By(ViewImpl.accountPermalink, accountID.value) + ) + + //loop over them and delete + var privilegesDeleted = true + views.map (x => { + privilegesDeleted &&= ViewPrivileges.bulkDelete_!!(By(ViewPrivileges.view, x.id_)) + }) + + //delete views of account + val viewsDeleted = ViewImpl.bulkDelete_!!( + By(ViewImpl.bankPermalink, bankId.value), + By(ViewImpl.accountPermalink, accountID.value) + ) + + //delete account + val account = MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, accountID.value) + ) + + val accountDeleted = account match { + case Full(acc) => acc.delete_! + case _ => false + } + + commentsDeleted && narrativesDeleted && tagsDeleted && whereTagsDeleted && transactionImagesDeleted && + transactionsDeleted && privilegesDeleted && viewsDeleted && accountDeleted +} + + //creates a bank account for an existing bank, with the appropriate values set. Can fail if the bank doesn't exist + override def createSandboxBankAccount(bankId: BankId, accountID: AccountId, accountNumber: String, + currency: String, initialBalance: BigDecimal, accountHolderName: String): Box[BankAccount] = { + + for { + bank <- getBank(bankId) //bank is not really used, but doing this will ensure account creations fails if the bank doesn't + } yield { + + val balanceInSmallestCurrencyUnits = Helper.convertToSmallestCurrencyUnits(initialBalance, currency) + createAccountIfNotExisting(bankId, accountID, accountNumber, currency, balanceInSmallestCurrencyUnits, accountHolderName) + } + + } + + //sets a user as an account owner/holder + override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = { + MappedAccountHolder.create + .accountBankPermalink(bankAccountUID.bankId.value) + .accountPermalink(bankAccountUID.accountId.value) + .user(user.apiId.value) + .save + } + + private def createAccountIfNotExisting(bankId: BankId, accountID: AccountId, accountNumber: String, + currency: String, balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = { + getBankAccountType(bankId, accountID) match { + case Full(a) => + logger.info(s"account with id $accountID at bank with id $bankId already exists. No need to create a new one.") + a + case _ => + MappedBankAccount.create + .bank(bankId.value) + .theAccountId(accountID.value) + .accountNumber(accountNumber) + .accountCurrency(currency) + .accountBalance(balanceInSmallestCurrencyUnits) + .holder(accountHolderName) + .saveMe() + } + } + + /* + End of bank account creation + */ + + /* + Cash api + */ + + //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountID + override def getAccountByUUID(uuid: String): Box[AccountType] = { + MappedBankAccount.find(By(MappedBankAccount.accUUID, uuid)) + } + + //cash api requires a call to add a new transaction and update the account balance + override def addCashTransactionAndUpdateBalance(account: AccountType, cashTransaction: CashTransaction): Unit = { + + val currency = account.currency + val currencyDecimalPlaces = Helper.currencyDecimalPlaces(currency) + + //not ideal to have to convert it this way + def doubleToSmallestCurrencyUnits(x : Double) : Long = { + (x * math.pow(10, currencyDecimalPlaces)).toLong + } + + //can't forget to set the sign of the amount cashed on kind being "in" or "out" + //we just assume if it's not "in", then it's "out" + val amountInSmallestCurrencyUnits = { + if(cashTransaction.kind == "in") doubleToSmallestCurrencyUnits(cashTransaction.amount) + else doubleToSmallestCurrencyUnits(-1 * cashTransaction.amount) + } + + val currentBalanceInSmallestCurrencyUnits = account.accountBalance.get + val newBalanceInSmallestCurrencyUnits = currentBalanceInSmallestCurrencyUnits + amountInSmallestCurrencyUnits + + //create transaction + val transactionCreated = MappedTransaction.create + .bank(account.bankId.value) + .account(account.accountId.value) + .transactionType("cash") + .amount(amountInSmallestCurrencyUnits) + .newAccountBalance(newBalanceInSmallestCurrencyUnits) + .currency(account.currency) + .tStartDate(cashTransaction.date) + .tFinishDate(cashTransaction.date) + .description(cashTransaction.label) + .counterpartyAccountHolder(cashTransaction.otherParty) + .counterpartyAccountKind("cash") + .save + + if(!transactionCreated) { + logger.warn("Failed to save cash transaction") + } else { + //update account + val accountUpdated = account.accountBalance(newBalanceInSmallestCurrencyUnits).save() + + if(!accountUpdated) + logger.warn("Failed to update account balance after new cash transaction") + } + } + + /* + End of cash api + */ + + /* + Transaction importer api + */ + + //used by the transaction import api + override def updateAccountBalance(bankId: BankId, accountID: AccountId, newBalance: BigDecimal): Boolean = { + + //this will be Full(true) if everything went well + val result = for { + acc <- getBankAccountType(bankId, accountID) + bank <- getMappedBank(bankId) + } yield { + acc.accountBalance(Helper.convertToSmallestCurrencyUnits(newBalance, acc.currency)).save + setBankAccountLastUpdated(bank.nationalIdentifier, acc.number, now) + } + + result.getOrElse(false) + } + + //transaction import api uses bank national identifiers to uniquely indentify banks, + //which is unfortunate as theoretically the national identifier is unique to a bank within + //one country + private def getBankByNationalIdentifier(nationalIdentifier : String) : Box[Bank] = { + MappedBank.find(By(MappedBank.national_identifier, nationalIdentifier)) + } + + private def getAccountByNumber(bankId : BankId, number : String) : Box[AccountType] = { + MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.accountNumber, number)) + } + + private val bigDecimalFailureHandler : PartialFunction[Throwable, Unit] = { + case ex : NumberFormatException => { + logger.warn(s"could not convert amount to a BigDecimal: $ex") + } + } + + //used by transaction import api call to check for duplicates + override def getMatchingTransactionCount(bankNationalIdentifier : String, accountNumber : String, amount: String, completed: Date, otherAccountHolder: String): Int = { + //we need to convert from the legacy bankNationalIdentifier to BankId, and from the legacy accountNumber to AccountId + val count = for { + bankId <- getBankByNationalIdentifier(bankNationalIdentifier).map(_.bankId) + account <- getAccountByNumber(bankId, accountNumber) + amountAsBigDecimal <- tryo(bigDecimalFailureHandler)(BigDecimal(amount)) + } yield { + + val amountInSmallestCurrencyUnits = + Helper.convertToSmallestCurrencyUnits(amountAsBigDecimal, account.currency) + + MappedTransaction.count( + By(MappedTransaction.bank, bankId.value), + By(MappedTransaction.account, account.accountId.value), + By(MappedTransaction.amount, amountInSmallestCurrencyUnits), + By(MappedTransaction.tFinishDate, completed), + By(MappedTransaction.counterpartyAccountHolder, otherAccountHolder)) + } + + //icky + count.map(_.toInt) getOrElse 0 + } + + //used by transaction import api + override def createImportedTransaction(transaction: ImporterTransaction): Box[Transaction] = { + //we need to convert from the legacy bankNationalIdentifier to BankId, and from the legacy accountNumber to AccountId + val obpTransaction = transaction.obp_transaction + val thisAccount = obpTransaction.this_account + val nationalIdentifier = thisAccount.bank.national_identifier + val accountNumber = thisAccount.number + for { + bank <- getBankByNationalIdentifier(transaction.obp_transaction.this_account.bank.national_identifier) ?~! + s"No bank found with national identifier $nationalIdentifier" + bankId = bank.bankId + account <- getAccountByNumber(bankId, accountNumber) + details = obpTransaction.details + amountAsBigDecimal <- tryo(bigDecimalFailureHandler)(BigDecimal(details.value.amount)) + newBalanceAsBigDecimal <- tryo(bigDecimalFailureHandler)(BigDecimal(details.new_balance.amount)) + amountInSmallestCurrencyUnits = Helper.convertToSmallestCurrencyUnits(amountAsBigDecimal, account.currency) + newBalanceInSmallestCurrencyUnits = Helper.convertToSmallestCurrencyUnits(newBalanceAsBigDecimal, account.currency) + otherAccount = obpTransaction.other_account + mappedTransaction = MappedTransaction.create + .bank(bankId.value) + .account(account.accountId.value) + .transactionType(details.kind) + .amount(amountInSmallestCurrencyUnits) + .newAccountBalance(newBalanceInSmallestCurrencyUnits) + .currency(account.currency) + .tStartDate(details.posted.`$dt`) + .tFinishDate(details.completed.`$dt`) + .description(details.label) + .counterpartyAccountNumber(otherAccount.number) + .counterpartyAccountHolder(otherAccount.holder) + .counterpartyAccountKind(otherAccount.kind) + .counterpartyNationalId(otherAccount.bank.national_identifier) + .counterpartyBankName(otherAccount.bank.name) + .counterpartyIban(otherAccount.bank.IBAN) + .saveMe() + transaction <- mappedTransaction.toTransaction(account) + } yield transaction + } + + override def setBankAccountLastUpdated(bankNationalIdentifier: String, accountNumber : String, updateDate: Date) : Boolean = { + val result = for { + bankId <- getBankByNationalIdentifier(bankNationalIdentifier).map(_.bankId) + account <- getAccountByNumber(bankId, accountNumber) + } yield { + val acc = MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, account.accountId.value) + ) + acc match { + case Full(a) => a.accountLastUpdate(updateDate).save + case _ => logger.warn("can't set bank account.lastUpdated because the account was not found"); false + } + } + result.getOrElse(false) + } + + /* + End of transaction importer api + */ + + + override def updateAccountLabel(bankId: BankId, accountID: AccountId, label: String): Boolean = { + //this will be Full(true) if everything went well + val result = for { + acc <- getBankAccountType(bankId, accountID) + bank <- getMappedBank(bankId) + } yield { + acc.accountLabel(label).save + } + + result.getOrElse(false) + } + +} + From 945cc8cf1dd8d5d7b7a825ac735fe4869f574f28 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 29 Dec 2015 11:21:04 +0100 Subject: [PATCH 277/702] Complete merge with kafka-connector branch --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 103628ac7..56d64c7eb 100644 --- a/pom.xml +++ b/pom.xml @@ -66,9 +66,9 @@ 0.10.1 - ch.qos.logback - logback-classic - 1.0.9 + org.apache.kafka + kafka_2.10 + 0.8.2.2 org.bouncycastle From 2af0ae916d9c559eac15d9b82fdbe9ab2a2ff2b2 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 13 Jan 2016 23:48:16 +0100 Subject: [PATCH 278/702] Start of v2.0.0 and version specific resource docs --- src/main/scala/bootstrap/liftweb/Boot.scala | 1 + .../scala/code/api/v1_2_1/OBPAPI1.2.1.scala | 10 +- .../scala/code/api/v1_3_0/OBPAPI1_3_0.scala | 9 +- .../scala/code/api/v1_4_0/APIMethods140.scala | 49 +- .../scala/code/api/v1_4_0/OBPAPI1_4_0.scala | 4 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 92 +++ .../code/api/v2_0_0/JSONFactory2.0.0.scala | 764 ++++++++++++++++++ .../scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 156 ++++ 8 files changed, 1070 insertions(+), 15 deletions(-) create mode 100644 src/main/scala/code/api/v2_0_0/APIMethods200.scala create mode 100644 src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala create mode 100644 src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 5804795e1..b79615e57 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -188,6 +188,7 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(v1_2_1.OBPAPI1_2_1) LiftRules.statelessDispatch.append(v1_3_0.OBPAPI1_3_0) LiftRules.statelessDispatch.append(v1_4_0.OBPAPI1_4_0) + LiftRules.statelessDispatch.append(v2_0_0.OBPAPI2_0_0) //add management apis LiftRules.statelessDispatch.append(ImporterAPI) diff --git a/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala index 283865639..411e5033e 100644 --- a/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala +++ b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala @@ -36,7 +36,10 @@ import net.liftweb.json.JsonAST._ import net.liftweb.common.Loggable import code.api.OBPRestHelper -object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable { + +import code.api.v1_4_0.APIMethods140 // Added so we can add resource docs for this version of the API + +object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable with APIMethods140 { val VERSION = "1.2.1" @@ -112,7 +115,10 @@ object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable { Implementations1_2_1.updateWhereTagForViewOnTransaction, Implementations1_2_1.deleteWhereTagForViewOnTransaction, Implementations1_2_1.getCounterpartyForTransaction, - Implementations1_2_1.makePayment + Implementations1_2_1.makePayment, + // For Resource Docs on this (1.2.1) version of the API + Implementations1_4_0.getResourceDocsObp(VERSION), + Implementations1_4_0.getResourceDocsSwagger(VERSION) ) routes.foreach(route => { diff --git a/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala b/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala index 592f30ad4..1e17f2188 100644 --- a/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala +++ b/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala @@ -4,9 +4,11 @@ import code.api.OBPRestHelper import code.api.v1_2_1.APIMethods121 import net.liftweb.common.Loggable +import code.api.v1_4_0.APIMethods140 // Added so we can add resource docs for this version of the API + //has APIMethods121 as all api calls that went unchanged from 1.2.1 to 1.3.0 will use the old //implementation -object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 with Loggable { +object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 with Loggable with APIMethods140 { val VERSION = "1.3.0" @@ -83,7 +85,10 @@ object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 w Implementations1_2_1.getCounterpartyForTransaction, Implementations1_2_1.makePayment, Implementations1_3_0.getCards, - Implementations1_3_0.getCardsForBank + Implementations1_3_0.getCardsForBank, + // For Resource Docs on this (1.3.0) version of the API + Implementations1_4_0.getResourceDocsObp(VERSION), + Implementations1_4_0.getResourceDocsSwagger(VERSION) ) routes.foreach(route => { diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 4a1b660be..230a0dac4 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -24,6 +24,7 @@ import collection.mutable.ArrayBuffer import code.api.APIFailure import code.api.v1_2_1.APIMethods121 import code.api.v1_3_0.APIMethods130 +import code.api.v2_0_0.APIMethods200 // So we can include resource docs from future versions import code.api.v1_4_0.JSONFactory1_4_0._ import code.atms.Atms import code.branches.Branches @@ -36,7 +37,9 @@ import code.util.Helper._ import code.api.util.APIUtil.ResourceDoc import java.text.SimpleDateFormat -trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ +import net.liftweb.http.CurrentReq + +trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with APIMethods121{ //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. // We add previous APIMethods so we have access to the Resource Docs self: RestHelper => @@ -52,11 +55,39 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ val exampleDate = simpleDateFormat.parse(exampleDateString) - def getResourceDocsList : Option[List[ResourceDoc]] = + + + def getResourceDocsList(requestedApiVersion : String) : Option[List[ResourceDoc]] = { - // Get the Resource Docs for this and previous versions of the API - val cumulativeResourceDocs = resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs - // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. + + // We'll have a different list of resource docs depending on the version being called. + // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) + + val path = CurrentReq.value.path + + logger.info(s"path is $path") + + val activeVersion = CurrentReq.value.path(1) + + logger.info(s"ver in path is $activeVersion") + + val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + + val cumulativeResourceDocs = requestedApiVersion match { + case "2.0.0" => resourceDocs2_0_0 + case "1.4.0" => resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + case "1.2.1" => Implementations1_2_1.resourceDocs + case _ => resourceDocs2_0_0 // Not sure if we should have a default here. + } + + // TODO Filter out non available APIs for the active version + // e.g. For each version we should only include those resourceDocs that are listed in the routes List + // (e.g. OBPAPI1_2_1 routes) unless we are in dev mode (Props.devMode is true) otherwise developers will see a huge list of APIs and get a bunch of 404s + + + + // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. Some(cumulativeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) } @@ -321,11 +352,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ // Provides resource documents so that API Explorer (or other apps) can display API documentation // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple various of markdown. - lazy val getResourceDocsObp : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + def getResourceDocsObp(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "obp" :: Nil JsonGet _ => { user => { for { - rd <- getResourceDocsList + rd <- getResourceDocsList(requestedApiVersion) } yield { // Format the data as json val json = JSONFactory1_4_0.createResourceDocsJson(rd) @@ -351,11 +382,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ emptyObjectJson :: Nil ) - lazy val getResourceDocsSwagger : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + def getResourceDocsSwagger(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "swagger" :: Nil JsonGet _ => { user => { for { - rd <- getResourceDocsList + rd <- getResourceDocsList(requestedApiVersion) } yield { // Format the data as json val json = SwaggerJSONFactory1_4_0.createSwaggerResourceDoc(rd) diff --git a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala index 19155efbd..3eae0ded6 100644 --- a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala @@ -93,8 +93,8 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable { Implementations1_4_0.getAtms, Implementations1_4_0.getProducts, Implementations1_4_0.getCrmEvents, - Implementations1_4_0.getResourceDocsObp, - Implementations1_4_0.getResourceDocsSwagger, + Implementations1_4_0.getResourceDocsObp(VERSION), + Implementations1_4_0.getResourceDocsSwagger(VERSION), Implementations1_4_0.createTransactionRequest, Implementations1_4_0.getTransactionRequests, Implementations1_4_0.getTransactionRequestTypes, diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala new file mode 100644 index 000000000..41bdb87e7 --- /dev/null +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -0,0 +1,92 @@ +package code.api.v2_0_0 + +import code.api.util.APIUtil +import code.api.v1_2_1.APIMethods121 +import net.liftweb.http.{JsonResponse, Req} +import net.liftweb.json.Extraction +import net.liftweb.common._ +import code.model._ +import net.liftweb.json.JsonAST.JValue +import APIUtil._ +import net.liftweb.util.Helpers._ +import net.liftweb.http.rest.RestHelper +import java.net.URL +import net.liftweb.util.Props +import code.bankconnectors._ +import code.bankconnectors.OBPOffset +import code.bankconnectors.OBPFromDate +import code.bankconnectors.OBPToDate +import code.model.ViewCreationJSON +import net.liftweb.common.Full +import code.model.ViewUpdateData + +import scala.collection.immutable.Nil +import scala.collection.mutable.ArrayBuffer +// Makes JValue assignment to Nil work +import net.liftweb.json.JsonDSL._ + + +trait APIMethods200 { + //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. + self: RestHelper => + + // helper methods begin here + + // New 2.0.0 + private def bankAccountBasicListToJson(bankAccounts: List[BankAccount], user : Box[User]): JValue = { + Extraction.decompose(bankAccountBasicList(bankAccounts, user)) + } + + private def bankAccountBasicList(bankAccounts: List[BankAccount], user : Box[User]): List[AccountBasicJSON] = { + val accJson : List[AccountBasicJSON] = bankAccounts.map( account => { + val views = account.permittedViews(user) + val viewsAvailable : List[ViewBasicJSON] = + views.map( v => { + JSONFactory.createViewBasicJSON(v) + }) + JSONFactory.createAccountBasicJSON(account,viewsAvailable) + }) + accJson + } + + + // helper methods end here + + val Implementations2_0_0 = new Object(){ + + val resourceDocs = ArrayBuffer[ResourceDoc]() + val emptyObjectJson : JValue = Nil + val apiVersion : String = "2_0_0" + + resourceDocs += ResourceDoc( + apiVersion, + "allAccountsAllBanks", + "GET", + "/accounts", + "Get all accounts a user has access to at all banks (private + public)", + """Returns the list of accounts at that the user has access to at all banks. + |For each account the API returns the account ID and the available views. + | + |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views. If + |the user is authenticated, the list will contain non-public accounts to which the user has access, in addition to + |all public accounts. + |""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get accounts for all banks (private + public) + case "accounts" :: Nil JsonGet json => { + user => + Full(successJsonResponse(bankAccountBasicListToJson(BankAccount.accounts(user), user))) + } + } + + + + } +} + +object APIMethods200 { +} diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala new file mode 100644 index 000000000..74c2d6939 --- /dev/null +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -0,0 +1,764 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd + +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 / Music Pictures Ltd +Osloerstrasse 16/17 +Berlin 13359, Germany + + This product includes software developed at + TESOBE (http://www.tesobe.com/) + by + Simon Redfern : simon AT tesobe DOT com + Stefan Bethge : stefan AT tesobe DOT com + Everett Sochowski : everett AT tesobe DOT com + Ayoub Benali: ayoub AT tesobe DOT com + + */ +package code.api.v2_0_0 + +import java.util.Date +import net.liftweb.common.{Box, Full} +import code.model._ + + + + + +// New in 2.0.0 + +class ViewBasicJSON( + val id: String, + val short_name: String, + val is_public: Boolean +) + +case class AccountsBasicJSON( + accounts : List[AccountBasicJSON] +) + +case class AccountBasicJSON( + id : String, + label : String, + views_available : List[ViewBasicJSON], + bank_id : String +) + + +// From 1.2.1 +// +case class APIInfoJSON( + version : String, + git_commit : String, + hosted_by : HostedBy +) +case class HostedBy( + organisation : String, + email : String, + phone : String +) +case class ErrorMessage( + error : String +) +case class SuccessMessage( + success : String +) +case class BanksJSON( + banks : List[BankJSON] +) +case class MinimalBankJSON( + national_identifier : String, + name : String +) +case class BankJSON( + id : String, + short_name : String, + full_name : String, + logo : String, + website : String +) +case class ViewsJSON( + views : List[ViewJSON] +) +class ViewJSON( + val id: String, + val short_name: String, + val description: String, + val is_public: Boolean, + val alias: String, + val hide_metadata_if_alias_used: Boolean, + val can_see_transaction_this_bank_account: Boolean, + val can_see_transaction_other_bank_account: Boolean, + val can_see_transaction_metadata: Boolean, + val can_see_transaction_description: Boolean, + val can_see_transaction_amount: Boolean, + val can_see_transaction_type: Boolean, + val can_see_transaction_currency: Boolean, + val can_see_transaction_start_date: Boolean, + val can_see_transaction_finish_date: Boolean, + val can_see_transaction_balance: Boolean, + val can_see_comments: Boolean, + val can_see_owner_comment: Boolean, + val can_see_tags: Boolean, + val can_see_images: Boolean, + val can_see_bank_account_owners: Boolean, + val can_see_bank_account_type: Boolean, + val can_see_bank_account_balance: Boolean, + val can_see_bank_account_currency: Boolean, + val can_see_bank_account_label: Boolean, + val can_see_bank_account_national_identifier: Boolean, + val can_see_bank_account_swift_bic: Boolean, + val can_see_bank_account_iban: Boolean, + val can_see_bank_account_number: Boolean, + val can_see_bank_account_bank_name: Boolean, + val can_see_other_account_national_identifier: Boolean, + val can_see_other_account_swift_bic: Boolean, + val can_see_other_account_iban: Boolean, + val can_see_other_account_bank_name: Boolean, + val can_see_other_account_number: Boolean, + val can_see_other_account_metadata: Boolean, + val can_see_other_account_kind: Boolean, + val can_see_more_info: Boolean, + val can_see_url: Boolean, + val can_see_image_url: Boolean, + val can_see_open_corporates_url: Boolean, + val can_see_corporate_location: Boolean, + val can_see_physical_location: Boolean, + val can_see_public_alias: Boolean, + val can_see_private_alias: Boolean, + val can_add_more_info: Boolean, + val can_add_url: Boolean, + val can_add_image_url: Boolean, + val can_add_open_corporates_url : Boolean, + val can_add_corporate_location : Boolean, + val can_add_physical_location : Boolean, + val can_add_public_alias : Boolean, + val can_add_private_alias : Boolean, + val can_delete_corporate_location : Boolean, + val can_delete_physical_location : Boolean, + val can_edit_owner_comment: Boolean, + val can_add_comment : Boolean, + val can_delete_comment: Boolean, + val can_add_tag : Boolean, + val can_delete_tag : Boolean, + val can_add_image : Boolean, + val can_delete_image : Boolean, + val can_add_where_tag : Boolean, + val can_see_where_tag : Boolean, + val can_delete_where_tag : Boolean +) +case class AccountsJSON( + accounts : List[AccountJSON] +) +case class AccountJSON( + id : String, + label : String, + views_available : List[ViewJSON], + bank_id : String +) + +case class UpdateAccountJSON( + id : String, + label : String, + bank_id : String +) + +case class ModeratedAccountJSON( + id : String, + label : String, + number : String, + owners : List[UserJSON], + `type` : String, + balance : AmountOfMoneyJSON, + IBAN : String, + swift_bic: String, + views_available : List[ViewJSON], + bank_id : String +) +case class UserJSON( + id : String, + provider : String, + display_name : String +) +case class PermissionsJSON( + permissions : List[PermissionJSON] +) +case class PermissionJSON( + user : UserJSON, + views : List[ViewJSON] +) +case class AmountOfMoneyJSON( + currency : String, + amount : String +) +case class AccountHolderJSON( + name : String, + is_alias : Boolean +) +case class ThisAccountJSON( + id : String, + holders : List[AccountHolderJSON], + number : String, + kind : String, + IBAN : String, + swift_bic: String, + bank : MinimalBankJSON +) +case class OtherAccountsJSON( + other_accounts : List[OtherAccountJSON] +) +case class OtherAccountJSON( + id : String, + holder : AccountHolderJSON, + number : String, + kind : String, + IBAN : String, + swift_bic: String, + bank : MinimalBankJSON, + metadata : OtherAccountMetadataJSON +) +case class OtherAccountMetadataJSON( + public_alias : String, + private_alias : String, + more_info : String, + URL : String, + image_URL : String, + open_corporates_URL : String, + corporate_location : LocationJSON, + physical_location : LocationJSON +) +case class LocationJSON( + latitude : Double, + longitude : Double, + date : Date, + user : UserJSON +) +case class TransactionDetailsJSON( + `type` : String, + description : String, + posted : Date, + completed : Date, + new_balance : AmountOfMoneyJSON, + value : AmountOfMoneyJSON +) +case class TransactionMetadataJSON( + narrative : String, + comments : List[TransactionCommentJSON], + tags : List[TransactionTagJSON], + images : List[TransactionImageJSON], + where : LocationJSON +) +case class TransactionsJSON( + transactions: List[TransactionJSON] +) +case class TransactionJSON( + id : String, + this_account : ThisAccountJSON, + other_account : OtherAccountJSON, + details : TransactionDetailsJSON, + metadata : TransactionMetadataJSON +) +case class TransactionImagesJSON( + images : List[TransactionImageJSON] +) +case class TransactionImageJSON( + id : String, + label : String, + URL : String, + date : Date, + user : UserJSON +) +case class PostTransactionImageJSON( + label : String, + URL : String +) +case class PostTransactionCommentJSON( + value: String +) +case class PostTransactionTagJSON( + value : String +) +case class TransactionTagJSON( + id : String, + value : String, + date : Date, + user : UserJSON +) +case class TransactionTagsJSON( + tags: List[TransactionTagJSON] +) +case class TransactionCommentJSON( + id : String, + value : String, + date: Date, + user : UserJSON +) +case class TransactionCommentsJSON( + comments: List[TransactionCommentJSON] +) +case class TransactionWhereJSON( + where: LocationJSON +) +case class PostTransactionWhereJSON( + where: LocationPlainJSON +) +case class AliasJSON( + alias: String +) +case class MoreInfoJSON( + more_info: String +) +case class UrlJSON( + URL:String +) +case class ImageUrlJSON( + image_URL: String +) +case class OpenCorporateUrlJSON( + open_corporates_URL: String +) +case class CorporateLocationJSON( + corporate_location: LocationPlainJSON +) +case class PhysicalLocationJSON( + physical_location: LocationPlainJSON +) +case class LocationPlainJSON( + latitude : Double, + longitude : Double +) +case class TransactionNarrativeJSON( + narrative : String +) + +case class ViewIdsJson( + views : List[String] +) + +object JSONFactory{ + + + // New in 2.0.0 + + def createViewBasicJSON(view : View) : ViewBasicJSON = { + val alias = + if(view.usePublicAliasIfOneExists) + "public" + else if(view.usePrivateAliasIfOneExists) + "private" + else + "" + + new ViewBasicJSON( + id = view.viewId.value, + short_name = stringOrNull(view.name), + is_public = view.isPublic + ) + } + + + + + def createAccountBasicJSON(account : BankAccount, viewsBasicAvailable : List[ViewBasicJSON] ) : AccountBasicJSON = { + new AccountBasicJSON( + account.accountId.value, + stringOrNull(account.label), + viewsBasicAvailable, + account.bankId.value + ) + } + + + + + + + + + + // From 1.2.1 + +// +// + def stringOrNull(text : String) = + if(text == null || text.isEmpty) + null + else + text + + def stringOptionOrNull(text : Option[String]) = + text match { + case Some(t) => stringOrNull(t) + case _ => null + } + + def createBankJSON(bank : Bank) : BankJSON = { + new BankJSON( + stringOrNull(bank.bankId.value), + stringOrNull(bank.shortName), + stringOrNull(bank.fullName), + stringOrNull(bank.logoUrl), + stringOrNull(bank.websiteUrl) + ) + } + + def createViewsJSON(views : List[View]) : ViewsJSON = { + val list : List[ViewJSON] = views.map(createViewJSON) + new ViewsJSON(list) + } + + def createViewJSON(view : View) : ViewJSON = { + val alias = + if(view.usePublicAliasIfOneExists) + "public" + else if(view.usePrivateAliasIfOneExists) + "private" + else + "" + + new ViewJSON( + id = view.viewId.value, + short_name = stringOrNull(view.name), + description = stringOrNull(view.description), + is_public = view.isPublic, + alias = alias, + hide_metadata_if_alias_used = view.hideOtherAccountMetadataIfAlias, + can_see_transaction_this_bank_account = view.canSeeTransactionThisBankAccount, + can_see_transaction_other_bank_account = view.canSeeTransactionOtherBankAccount, + can_see_transaction_metadata = view.canSeeTransactionMetadata, + can_see_transaction_description = view.canSeeTransactionDescription, + can_see_transaction_amount = view.canSeeTransactionAmount, + can_see_transaction_type = view.canSeeTransactionType, + can_see_transaction_currency = view.canSeeTransactionCurrency, + can_see_transaction_start_date = view.canSeeTransactionStartDate, + can_see_transaction_finish_date = view.canSeeTransactionFinishDate, + can_see_transaction_balance = view.canSeeTransactionBalance, + can_see_comments = view.canSeeComments, + can_see_owner_comment = view.canSeeOwnerComment, + can_see_tags = view.canSeeTags, + can_see_images = view.canSeeImages, + can_see_bank_account_owners = view.canSeeBankAccountOwners, + can_see_bank_account_type = view.canSeeBankAccountType, + can_see_bank_account_balance = view.canSeeBankAccountBalance, + can_see_bank_account_currency = view.canSeeBankAccountCurrency, + can_see_bank_account_label = view.canSeeBankAccountLabel, + can_see_bank_account_national_identifier = view.canSeeBankAccountNationalIdentifier, + can_see_bank_account_swift_bic = view.canSeeBankAccountSwift_bic, + can_see_bank_account_iban = view.canSeeBankAccountIban, + can_see_bank_account_number = view.canSeeBankAccountNumber, + can_see_bank_account_bank_name = view.canSeeBankAccountBankName, + can_see_other_account_national_identifier = view.canSeeOtherAccountNationalIdentifier, + can_see_other_account_swift_bic = view.canSeeOtherAccountSWIFT_BIC, + can_see_other_account_iban = view.canSeeOtherAccountIBAN, + can_see_other_account_bank_name = view.canSeeOtherAccountBankName, + can_see_other_account_number = view.canSeeOtherAccountNumber, + can_see_other_account_metadata = view.canSeeOtherAccountMetadata, + can_see_other_account_kind = view.canSeeOtherAccountKind, + can_see_more_info = view.canSeeMoreInfo, + can_see_url = view.canSeeUrl, + can_see_image_url = view.canSeeImageUrl, + can_see_open_corporates_url = view.canSeeOpenCorporatesUrl, + can_see_corporate_location = view.canSeeCorporateLocation, + can_see_physical_location = view.canSeePhysicalLocation, + can_see_public_alias = view.canSeePublicAlias, + can_see_private_alias = view.canSeePrivateAlias, + can_add_more_info = view.canAddMoreInfo, + can_add_url = view.canAddURL, + can_add_image_url = view.canAddImageURL, + can_add_open_corporates_url = view.canAddOpenCorporatesUrl, + can_add_corporate_location = view.canAddCorporateLocation, + can_add_physical_location = view.canAddPhysicalLocation, + can_add_public_alias = view.canAddPublicAlias, + can_add_private_alias = view.canAddPrivateAlias, + can_delete_corporate_location = view.canDeleteCorporateLocation, + can_delete_physical_location = view.canDeletePhysicalLocation, + can_edit_owner_comment = view.canEditOwnerComment, + can_add_comment = view.canAddComment, + can_delete_comment = view.canDeleteComment, + can_add_tag = view.canAddTag, + can_delete_tag = view.canDeleteTag, + can_add_image = view.canAddImage, + can_delete_image = view.canDeleteImage, + can_add_where_tag = view.canAddWhereTag, + can_see_where_tag = view.canSeeWhereTag, + can_delete_where_tag = view.canDeleteWhereTag + ) + } + + def createAccountJSON(account : BankAccount, viewsAvailable : List[ViewJSON] ) : AccountJSON = { + new AccountJSON( + account.accountId.value, + stringOrNull(account.label), + viewsAvailable, + account.bankId.value + ) + } + + def createBankAccountJSON(account : ModeratedBankAccount, viewsAvailable : List[ViewJSON]) : ModeratedAccountJSON = { + val bankName = account.bankName.getOrElse("") + new ModeratedAccountJSON( + account.accountId.value, + stringOptionOrNull(account.label), + stringOptionOrNull(account.number), + createOwnersJSON(account.owners.getOrElse(Set()), bankName), + stringOptionOrNull(account.accountType), + createAmountOfMoneyJSON(account.currency.getOrElse(""), account.balance), + stringOptionOrNull(account.iban), + stringOptionOrNull(account.swift_bic), + viewsAvailable, + stringOrNull(account.bankId.value) + ) + } + + def createTransactionsJSON(transactions: List[ModeratedTransaction]) : TransactionsJSON = { + new TransactionsJSON(transactions.map(createTransactionJSON)) + } + + def createTransactionJSON(transaction : ModeratedTransaction) : TransactionJSON = { + new TransactionJSON( + id = transaction.id.value, + this_account = transaction.bankAccount.map(createThisAccountJSON).getOrElse(null), + other_account = transaction.otherBankAccount.map(createOtherBankAccount).getOrElse(null), + details = createTransactionDetailsJSON(transaction), + metadata = transaction.metadata.map(createTransactionMetadataJSON).getOrElse(null) + ) + } + + def createTransactionCommentsJSON(comments : List[Comment]) : TransactionCommentsJSON = { + new TransactionCommentsJSON(comments.map(createTransactionCommentJSON)) + } + + def createTransactionCommentJSON(comment : Comment) : TransactionCommentJSON = { + new TransactionCommentJSON( + id = comment.id_, + value = comment.text, + date = comment.datePosted, + user = createUserJSON(comment.postedBy) + ) + } + + def createTransactionImagesJSON(images : List[TransactionImage]) : TransactionImagesJSON = { + new TransactionImagesJSON(images.map(createTransactionImageJSON)) + } + + def createTransactionImageJSON(image : TransactionImage) : TransactionImageJSON = { + new TransactionImageJSON( + id = image.id_, + label = image.description, + URL = image.imageUrl.toString, + date = image.datePosted, + user = createUserJSON(image.postedBy) + ) + } + + def createTransactionTagsJSON(tags : List[TransactionTag]) : TransactionTagsJSON = { + new TransactionTagsJSON(tags.map(createTransactionTagJSON)) + } + + def createTransactionTagJSON(tag : TransactionTag) : TransactionTagJSON = { + new TransactionTagJSON( + id = tag.id_, + value = tag.value, + date = tag.datePosted, + user = createUserJSON(tag.postedBy) + ) + } + + def createLocationJSON(loc : Option[GeoTag]) : LocationJSON = { + loc match { + case Some(location) => { + val user = createUserJSON(location.postedBy) + //test if the GeoTag is set to its default value + if(location.latitude == 0.0 & location.longitude == 0.0 & user == null) + null + else + new LocationJSON( + latitude = location.latitude, + longitude = location.longitude, + date = location.datePosted, + user = user + ) + } + case _ => null + } + } + + def createLocationPlainJSON(lat: Double, lon: Double) : LocationPlainJSON = { + new LocationPlainJSON( + latitude = lat, + longitude = lon + ) + } + + def createTransactionMetadataJSON(metadata : ModeratedTransactionMetadata) : TransactionMetadataJSON = { + new TransactionMetadataJSON( + narrative = stringOptionOrNull(metadata.ownerComment), + comments = metadata.comments.map(_.map(createTransactionCommentJSON)).getOrElse(null), + tags = metadata.tags.map(_.map(createTransactionTagJSON)).getOrElse(null), + images = metadata.images.map(_.map(createTransactionImageJSON)).getOrElse(null), + where = metadata.whereTag.map(createLocationJSON).getOrElse(null) + ) + } + + def createTransactionDetailsJSON(transaction : ModeratedTransaction) : TransactionDetailsJSON = { + new TransactionDetailsJSON( + `type` = stringOptionOrNull(transaction.transactionType), + description = stringOptionOrNull(transaction.description), + posted = transaction.startDate.getOrElse(null), + completed = transaction.finishDate.getOrElse(null), + new_balance = createAmountOfMoneyJSON(transaction.currency, transaction.balance), + value= createAmountOfMoneyJSON(transaction.currency, transaction.amount.map(_.toString)) + ) + } + + def createMinimalBankJSON(bankAccount : ModeratedBankAccount) : MinimalBankJSON = { + new MinimalBankJSON( + national_identifier = stringOptionOrNull(bankAccount.nationalIdentifier), + name = stringOptionOrNull(bankAccount.bankName) + ) + } + + def createMinimalBankJSON(bankAccount : ModeratedOtherBankAccount) : MinimalBankJSON = { + new MinimalBankJSON( + national_identifier = stringOptionOrNull(bankAccount.nationalIdentifier), + name = stringOptionOrNull(bankAccount.bankName) + ) + } + + def createThisAccountJSON(bankAccount : ModeratedBankAccount) : ThisAccountJSON = { + new ThisAccountJSON( + id = bankAccount.accountId.value, + number = stringOptionOrNull(bankAccount.number), + kind = stringOptionOrNull(bankAccount.accountType), + IBAN = stringOptionOrNull(bankAccount.iban), + swift_bic = stringOptionOrNull(bankAccount.swift_bic), + bank = createMinimalBankJSON(bankAccount), + holders = bankAccount.owners.map(x => x.toList.map(h => createAccountHolderJSON(h, false))).getOrElse(null) + ) + } + + def createAccountHolderJSON(owner : User, isAlias : Boolean) : AccountHolderJSON = { + new AccountHolderJSON( + name = owner.name, + is_alias = isAlias + ) + } + + def createAccountHolderJSON(name : String, isAlias : Boolean) : AccountHolderJSON = { + new AccountHolderJSON( + name = name, + is_alias = isAlias + ) + } + + def createOtherAccountMetaDataJSON(metadata : ModeratedOtherBankAccountMetadata) : OtherAccountMetadataJSON = { + new OtherAccountMetadataJSON( + public_alias = stringOptionOrNull(metadata.publicAlias), + private_alias = stringOptionOrNull(metadata.privateAlias), + more_info = stringOptionOrNull(metadata.moreInfo), + URL = stringOptionOrNull(metadata.url), + image_URL = stringOptionOrNull(metadata.imageURL), + open_corporates_URL = stringOptionOrNull(metadata.openCorporatesURL), + corporate_location = metadata.corporateLocation.map(createLocationJSON).getOrElse(null), + physical_location = metadata.physicalLocation.map(createLocationJSON).getOrElse(null) + ) + } + + def createOtherBankAccount(bankAccount : ModeratedOtherBankAccount) : OtherAccountJSON = { + new OtherAccountJSON( + id = bankAccount.id, + number = stringOptionOrNull(bankAccount.number), + kind = stringOptionOrNull(bankAccount.kind), + IBAN = stringOptionOrNull(bankAccount.iban), + swift_bic = stringOptionOrNull(bankAccount.swift_bic), + bank = createMinimalBankJSON(bankAccount), + holder = createAccountHolderJSON(bankAccount.label.display, bankAccount.isAlias), + metadata = bankAccount.metadata.map(createOtherAccountMetaDataJSON).getOrElse(null) + ) + } + + def createOtherBankAccountsJSON(otherBankAccounts : List[ModeratedOtherBankAccount]) : OtherAccountsJSON = { + val otherAccountsJSON : List[OtherAccountJSON] = otherBankAccounts.map(createOtherBankAccount) + OtherAccountsJSON(otherAccountsJSON) + } + + def createUserJSON(user : User) : UserJSON = { + new UserJSON( + user.idGivenByProvider, + stringOrNull(user.provider), + stringOrNull(user.emailAddress) //TODO: shouldn't this be the display name? + ) + } + + def createUserJSON(user : Box[User]) : UserJSON = { + user match { + case Full(u) => createUserJSON(u) + case _ => null + } + } + + def createOwnersJSON(owners : Set[User], bankName : String) : List[UserJSON] = { + owners.map(o => { + new UserJSON( + o.idGivenByProvider, + stringOrNull(o.provider), + stringOrNull(o.name) + ) + } + ).toList + } + + def createAmountOfMoneyJSON(currency : String, amount : String) : AmountOfMoneyJSON = { + new AmountOfMoneyJSON( + stringOrNull(currency), + stringOrNull(amount) + ) + } + + def createAmountOfMoneyJSON(currency : Option[String], amount : Option[String]) : AmountOfMoneyJSON = { + new AmountOfMoneyJSON( + stringOptionOrNull(currency), + stringOptionOrNull(amount) + ) + } + + def createAmountOfMoneyJSON(currency : Option[String], amount : String) : AmountOfMoneyJSON = { + new AmountOfMoneyJSON( + stringOptionOrNull(currency), + stringOrNull(amount) + ) + } + + def createPermissionsJSON(permissions : List[Permission]) : PermissionsJSON = { + val permissionsJson = permissions.map(p => { + new PermissionJSON( + createUserJSON(p.user), + p.views.map(createViewJSON) + ) + }) + new PermissionsJSON(permissionsJson) + } + + def createAliasJSON(alias: String): AliasJSON = { + AliasJSON(stringOrNull(alias)) + } + + def createTransactionNarrativeJSON(narrative: String): TransactionNarrativeJSON = { + TransactionNarrativeJSON(stringOrNull(narrative)) + } + +} \ No newline at end of file diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala new file mode 100644 index 000000000..d4b8f33db --- /dev/null +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -0,0 +1,156 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd + +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 / Music Pictures Ltd +Osloerstrasse 16/17 +Berlin 13359, Germany + + This product includes software developed at + TESOBE (http://www.tesobe.com/) + by + Simon Redfern : simon AT tesobe DOT com + Stefan Bethge : stefan AT tesobe DOT com + Everett Sochowski : everett AT tesobe DOT com + Ayoub Benali: ayoub AT tesobe DOT com + + */ +package code.api.v2_0_0 + +import code.api.v1_3_0.APIMethods130 +import code.api.v1_4_0.APIMethods140 +import net.liftweb.json.Extraction +import net.liftweb.json.JsonAST._ +import net.liftweb.common.Loggable +import code.api.OBPRestHelper + +object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 with APIMethods200 with Loggable { + + + val VERSION = "2.0.0" + + val routes = List( + Implementations1_2_1.root(VERSION), + Implementations1_2_1.allBanks, + Implementations1_2_1.bankById, + // now in v2.0.0 Implementations2_0_0.allAccountsAllBanks + Implementations1_2_1.privateAccountsAllBanks, + Implementations1_2_1.publicAccountsAllBanks, + Implementations1_2_1.allAccountsAtOneBank, + Implementations1_2_1.privateAccountsAtOneBank, + Implementations1_2_1.publicAccountsAtOneBank, + Implementations1_2_1.accountById, + Implementations1_2_1.updateAccountLabel, + Implementations1_2_1.getViewsForBankAccount, + Implementations1_2_1.createViewForBankAccount, + Implementations1_2_1.updateViewForBankAccount, + Implementations1_2_1.deleteViewForBankAccount, + Implementations1_2_1.getPermissionsForBankAccount, + Implementations1_2_1.getPermissionForUserForBankAccount, + Implementations1_2_1.addPermissionForUserForBankAccountForMultipleViews, + Implementations1_2_1.addPermissionForUserForBankAccountForOneView, + Implementations1_2_1.removePermissionForUserForBankAccountForOneView, + Implementations1_2_1.removePermissionForUserForBankAccountForAllViews, + Implementations1_2_1.getCounterpartiesForBankAccount, + Implementations1_2_1.getCounterpartyByIdForBankAccount, + Implementations1_2_1.getCounterpartyMetadata, + Implementations1_2_1.getCounterpartyPublicAlias, + Implementations1_2_1.addCounterpartyPublicAlias, + Implementations1_2_1.updateCounterpartyPublicAlias, + Implementations1_2_1.deleteCounterpartyPublicAlias, + Implementations1_2_1.getCounterpartyPrivateAlias, + Implementations1_2_1.addCounterpartyPrivateAlias, + Implementations1_2_1.updateCounterpartyPrivateAlias, + Implementations1_2_1.deleteCounterpartyPrivateAlias, + Implementations1_2_1.addCounterpartyMoreInfo, + Implementations1_2_1.updateCounterpartyMoreInfo, + Implementations1_2_1.deleteCounterpartyMoreInfo, + Implementations1_2_1.addCounterpartyUrl, + Implementations1_2_1.updateCounterpartyUrl, + Implementations1_2_1.deleteCounterpartyUrl, + Implementations1_2_1.addCounterpartyImageUrl, + Implementations1_2_1.updateCounterpartyImageUrl, + Implementations1_2_1.deleteCounterpartyImageUrl, + Implementations1_2_1.addCounterpartyOpenCorporatesUrl, + Implementations1_2_1.updateCounterpartyOpenCorporatesUrl, + Implementations1_2_1.deleteCounterpartyOpenCorporatesUrl, + Implementations1_2_1.addCounterpartyCorporateLocation, + Implementations1_2_1.updateCounterpartyCorporateLocation, + Implementations1_2_1.deleteCounterpartyCorporateLocation, + Implementations1_2_1.addCounterpartyPhysicalLocation, + Implementations1_2_1.updateCounterpartyPhysicalLocation, + Implementations1_2_1.deleteCounterpartyPhysicalLocation, + Implementations1_2_1.getTransactionsForBankAccount, + Implementations1_2_1.getTransactionByIdForBankAccount, + Implementations1_2_1.getTransactionNarrative, + Implementations1_2_1.addTransactionNarrative, + Implementations1_2_1.updateTransactionNarrative, + Implementations1_2_1.deleteTransactionNarrative, + Implementations1_2_1.getCommentsForViewOnTransaction, + Implementations1_2_1.addCommentForViewOnTransaction, + Implementations1_2_1.deleteCommentForViewOnTransaction, + Implementations1_2_1.getTagsForViewOnTransaction, + Implementations1_2_1.addTagForViewOnTransaction, + Implementations1_2_1.deleteTagForViewOnTransaction, + Implementations1_2_1.getImagesForViewOnTransaction, + Implementations1_2_1.addImageForViewOnTransaction, + Implementations1_2_1.deleteImageForViewOnTransaction, + Implementations1_2_1.getWhereTagForViewOnTransaction, + Implementations1_2_1.addWhereTagForViewOnTransaction, + Implementations1_2_1.updateWhereTagForViewOnTransaction, + Implementations1_2_1.deleteWhereTagForViewOnTransaction, + Implementations1_2_1.getCounterpartyForTransaction, + Implementations1_2_1.makePayment, + + // New in 1.3.0 + Implementations1_3_0.getCards, + Implementations1_3_0.getCardsForBank, + + // New in 1.4.0 + Implementations1_4_0.getCustomer, + Implementations1_4_0.addCustomer, + Implementations1_4_0.getCustomerMessages, + Implementations1_4_0.addCustomerMessage, + Implementations1_4_0.getBranches, + Implementations1_4_0.getAtms, + Implementations1_4_0.getProducts, + Implementations1_4_0.getCrmEvents, + Implementations1_4_0.getResourceDocsObp(VERSION), + Implementations1_4_0.getResourceDocsSwagger(VERSION), + Implementations1_4_0.createTransactionRequest, + Implementations1_4_0.getTransactionRequests, + Implementations1_4_0.getTransactionRequestTypes, + Implementations1_4_0.answerTransactionRequestChallenge, + + // Modified in 2.0.0 + + Implementations2_0_0.allAccountsAllBanks +// Implementations1_2_1.privateAccountsAllBanks, +// Implementations1_2_1.publicAccountsAllBanks, +// Implementations1_2_1.allAccountsAtOneBank, +// Implementations1_2_1.privateAccountsAtOneBank, +// Implementations1_2_1.publicAccountsAtOneBank, +// Implementations1_2_1.accountById + ) + + routes.foreach(route => { + oauthServe(apiPrefix{route}) + }) + + + +} \ No newline at end of file From 4bb1acde4b3f6ece2a19ab49f9ab01982acc71c8 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 13 Jan 2016 23:53:04 +0100 Subject: [PATCH 279/702] cleaning up --- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 663 +----------------- 1 file changed, 1 insertion(+), 662 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 74c2d6939..ec12b85c6 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -59,296 +59,6 @@ case class AccountBasicJSON( ) -// From 1.2.1 -// -case class APIInfoJSON( - version : String, - git_commit : String, - hosted_by : HostedBy -) -case class HostedBy( - organisation : String, - email : String, - phone : String -) -case class ErrorMessage( - error : String -) -case class SuccessMessage( - success : String -) -case class BanksJSON( - banks : List[BankJSON] -) -case class MinimalBankJSON( - national_identifier : String, - name : String -) -case class BankJSON( - id : String, - short_name : String, - full_name : String, - logo : String, - website : String -) -case class ViewsJSON( - views : List[ViewJSON] -) -class ViewJSON( - val id: String, - val short_name: String, - val description: String, - val is_public: Boolean, - val alias: String, - val hide_metadata_if_alias_used: Boolean, - val can_see_transaction_this_bank_account: Boolean, - val can_see_transaction_other_bank_account: Boolean, - val can_see_transaction_metadata: Boolean, - val can_see_transaction_description: Boolean, - val can_see_transaction_amount: Boolean, - val can_see_transaction_type: Boolean, - val can_see_transaction_currency: Boolean, - val can_see_transaction_start_date: Boolean, - val can_see_transaction_finish_date: Boolean, - val can_see_transaction_balance: Boolean, - val can_see_comments: Boolean, - val can_see_owner_comment: Boolean, - val can_see_tags: Boolean, - val can_see_images: Boolean, - val can_see_bank_account_owners: Boolean, - val can_see_bank_account_type: Boolean, - val can_see_bank_account_balance: Boolean, - val can_see_bank_account_currency: Boolean, - val can_see_bank_account_label: Boolean, - val can_see_bank_account_national_identifier: Boolean, - val can_see_bank_account_swift_bic: Boolean, - val can_see_bank_account_iban: Boolean, - val can_see_bank_account_number: Boolean, - val can_see_bank_account_bank_name: Boolean, - val can_see_other_account_national_identifier: Boolean, - val can_see_other_account_swift_bic: Boolean, - val can_see_other_account_iban: Boolean, - val can_see_other_account_bank_name: Boolean, - val can_see_other_account_number: Boolean, - val can_see_other_account_metadata: Boolean, - val can_see_other_account_kind: Boolean, - val can_see_more_info: Boolean, - val can_see_url: Boolean, - val can_see_image_url: Boolean, - val can_see_open_corporates_url: Boolean, - val can_see_corporate_location: Boolean, - val can_see_physical_location: Boolean, - val can_see_public_alias: Boolean, - val can_see_private_alias: Boolean, - val can_add_more_info: Boolean, - val can_add_url: Boolean, - val can_add_image_url: Boolean, - val can_add_open_corporates_url : Boolean, - val can_add_corporate_location : Boolean, - val can_add_physical_location : Boolean, - val can_add_public_alias : Boolean, - val can_add_private_alias : Boolean, - val can_delete_corporate_location : Boolean, - val can_delete_physical_location : Boolean, - val can_edit_owner_comment: Boolean, - val can_add_comment : Boolean, - val can_delete_comment: Boolean, - val can_add_tag : Boolean, - val can_delete_tag : Boolean, - val can_add_image : Boolean, - val can_delete_image : Boolean, - val can_add_where_tag : Boolean, - val can_see_where_tag : Boolean, - val can_delete_where_tag : Boolean -) -case class AccountsJSON( - accounts : List[AccountJSON] -) -case class AccountJSON( - id : String, - label : String, - views_available : List[ViewJSON], - bank_id : String -) - -case class UpdateAccountJSON( - id : String, - label : String, - bank_id : String -) - -case class ModeratedAccountJSON( - id : String, - label : String, - number : String, - owners : List[UserJSON], - `type` : String, - balance : AmountOfMoneyJSON, - IBAN : String, - swift_bic: String, - views_available : List[ViewJSON], - bank_id : String -) -case class UserJSON( - id : String, - provider : String, - display_name : String -) -case class PermissionsJSON( - permissions : List[PermissionJSON] -) -case class PermissionJSON( - user : UserJSON, - views : List[ViewJSON] -) -case class AmountOfMoneyJSON( - currency : String, - amount : String -) -case class AccountHolderJSON( - name : String, - is_alias : Boolean -) -case class ThisAccountJSON( - id : String, - holders : List[AccountHolderJSON], - number : String, - kind : String, - IBAN : String, - swift_bic: String, - bank : MinimalBankJSON -) -case class OtherAccountsJSON( - other_accounts : List[OtherAccountJSON] -) -case class OtherAccountJSON( - id : String, - holder : AccountHolderJSON, - number : String, - kind : String, - IBAN : String, - swift_bic: String, - bank : MinimalBankJSON, - metadata : OtherAccountMetadataJSON -) -case class OtherAccountMetadataJSON( - public_alias : String, - private_alias : String, - more_info : String, - URL : String, - image_URL : String, - open_corporates_URL : String, - corporate_location : LocationJSON, - physical_location : LocationJSON -) -case class LocationJSON( - latitude : Double, - longitude : Double, - date : Date, - user : UserJSON -) -case class TransactionDetailsJSON( - `type` : String, - description : String, - posted : Date, - completed : Date, - new_balance : AmountOfMoneyJSON, - value : AmountOfMoneyJSON -) -case class TransactionMetadataJSON( - narrative : String, - comments : List[TransactionCommentJSON], - tags : List[TransactionTagJSON], - images : List[TransactionImageJSON], - where : LocationJSON -) -case class TransactionsJSON( - transactions: List[TransactionJSON] -) -case class TransactionJSON( - id : String, - this_account : ThisAccountJSON, - other_account : OtherAccountJSON, - details : TransactionDetailsJSON, - metadata : TransactionMetadataJSON -) -case class TransactionImagesJSON( - images : List[TransactionImageJSON] -) -case class TransactionImageJSON( - id : String, - label : String, - URL : String, - date : Date, - user : UserJSON -) -case class PostTransactionImageJSON( - label : String, - URL : String -) -case class PostTransactionCommentJSON( - value: String -) -case class PostTransactionTagJSON( - value : String -) -case class TransactionTagJSON( - id : String, - value : String, - date : Date, - user : UserJSON -) -case class TransactionTagsJSON( - tags: List[TransactionTagJSON] -) -case class TransactionCommentJSON( - id : String, - value : String, - date: Date, - user : UserJSON -) -case class TransactionCommentsJSON( - comments: List[TransactionCommentJSON] -) -case class TransactionWhereJSON( - where: LocationJSON -) -case class PostTransactionWhereJSON( - where: LocationPlainJSON -) -case class AliasJSON( - alias: String -) -case class MoreInfoJSON( - more_info: String -) -case class UrlJSON( - URL:String -) -case class ImageUrlJSON( - image_URL: String -) -case class OpenCorporateUrlJSON( - open_corporates_URL: String -) -case class CorporateLocationJSON( - corporate_location: LocationPlainJSON -) -case class PhysicalLocationJSON( - physical_location: LocationPlainJSON -) -case class LocationPlainJSON( - latitude : Double, - longitude : Double -) -case class TransactionNarrativeJSON( - narrative : String -) - -case class ViewIdsJson( - views : List[String] -) - object JSONFactory{ @@ -371,8 +81,6 @@ object JSONFactory{ } - - def createAccountBasicJSON(account : BankAccount, viewsBasicAvailable : List[ViewBasicJSON] ) : AccountBasicJSON = { new AccountBasicJSON( account.accountId.value, @@ -381,384 +89,15 @@ object JSONFactory{ account.bankId.value ) } - - - - - - - - - + // From 1.2.1 -// -// def stringOrNull(text : String) = if(text == null || text.isEmpty) null else text - def stringOptionOrNull(text : Option[String]) = - text match { - case Some(t) => stringOrNull(t) - case _ => null - } - def createBankJSON(bank : Bank) : BankJSON = { - new BankJSON( - stringOrNull(bank.bankId.value), - stringOrNull(bank.shortName), - stringOrNull(bank.fullName), - stringOrNull(bank.logoUrl), - stringOrNull(bank.websiteUrl) - ) - } - - def createViewsJSON(views : List[View]) : ViewsJSON = { - val list : List[ViewJSON] = views.map(createViewJSON) - new ViewsJSON(list) - } - - def createViewJSON(view : View) : ViewJSON = { - val alias = - if(view.usePublicAliasIfOneExists) - "public" - else if(view.usePrivateAliasIfOneExists) - "private" - else - "" - - new ViewJSON( - id = view.viewId.value, - short_name = stringOrNull(view.name), - description = stringOrNull(view.description), - is_public = view.isPublic, - alias = alias, - hide_metadata_if_alias_used = view.hideOtherAccountMetadataIfAlias, - can_see_transaction_this_bank_account = view.canSeeTransactionThisBankAccount, - can_see_transaction_other_bank_account = view.canSeeTransactionOtherBankAccount, - can_see_transaction_metadata = view.canSeeTransactionMetadata, - can_see_transaction_description = view.canSeeTransactionDescription, - can_see_transaction_amount = view.canSeeTransactionAmount, - can_see_transaction_type = view.canSeeTransactionType, - can_see_transaction_currency = view.canSeeTransactionCurrency, - can_see_transaction_start_date = view.canSeeTransactionStartDate, - can_see_transaction_finish_date = view.canSeeTransactionFinishDate, - can_see_transaction_balance = view.canSeeTransactionBalance, - can_see_comments = view.canSeeComments, - can_see_owner_comment = view.canSeeOwnerComment, - can_see_tags = view.canSeeTags, - can_see_images = view.canSeeImages, - can_see_bank_account_owners = view.canSeeBankAccountOwners, - can_see_bank_account_type = view.canSeeBankAccountType, - can_see_bank_account_balance = view.canSeeBankAccountBalance, - can_see_bank_account_currency = view.canSeeBankAccountCurrency, - can_see_bank_account_label = view.canSeeBankAccountLabel, - can_see_bank_account_national_identifier = view.canSeeBankAccountNationalIdentifier, - can_see_bank_account_swift_bic = view.canSeeBankAccountSwift_bic, - can_see_bank_account_iban = view.canSeeBankAccountIban, - can_see_bank_account_number = view.canSeeBankAccountNumber, - can_see_bank_account_bank_name = view.canSeeBankAccountBankName, - can_see_other_account_national_identifier = view.canSeeOtherAccountNationalIdentifier, - can_see_other_account_swift_bic = view.canSeeOtherAccountSWIFT_BIC, - can_see_other_account_iban = view.canSeeOtherAccountIBAN, - can_see_other_account_bank_name = view.canSeeOtherAccountBankName, - can_see_other_account_number = view.canSeeOtherAccountNumber, - can_see_other_account_metadata = view.canSeeOtherAccountMetadata, - can_see_other_account_kind = view.canSeeOtherAccountKind, - can_see_more_info = view.canSeeMoreInfo, - can_see_url = view.canSeeUrl, - can_see_image_url = view.canSeeImageUrl, - can_see_open_corporates_url = view.canSeeOpenCorporatesUrl, - can_see_corporate_location = view.canSeeCorporateLocation, - can_see_physical_location = view.canSeePhysicalLocation, - can_see_public_alias = view.canSeePublicAlias, - can_see_private_alias = view.canSeePrivateAlias, - can_add_more_info = view.canAddMoreInfo, - can_add_url = view.canAddURL, - can_add_image_url = view.canAddImageURL, - can_add_open_corporates_url = view.canAddOpenCorporatesUrl, - can_add_corporate_location = view.canAddCorporateLocation, - can_add_physical_location = view.canAddPhysicalLocation, - can_add_public_alias = view.canAddPublicAlias, - can_add_private_alias = view.canAddPrivateAlias, - can_delete_corporate_location = view.canDeleteCorporateLocation, - can_delete_physical_location = view.canDeletePhysicalLocation, - can_edit_owner_comment = view.canEditOwnerComment, - can_add_comment = view.canAddComment, - can_delete_comment = view.canDeleteComment, - can_add_tag = view.canAddTag, - can_delete_tag = view.canDeleteTag, - can_add_image = view.canAddImage, - can_delete_image = view.canDeleteImage, - can_add_where_tag = view.canAddWhereTag, - can_see_where_tag = view.canSeeWhereTag, - can_delete_where_tag = view.canDeleteWhereTag - ) - } - - def createAccountJSON(account : BankAccount, viewsAvailable : List[ViewJSON] ) : AccountJSON = { - new AccountJSON( - account.accountId.value, - stringOrNull(account.label), - viewsAvailable, - account.bankId.value - ) - } - - def createBankAccountJSON(account : ModeratedBankAccount, viewsAvailable : List[ViewJSON]) : ModeratedAccountJSON = { - val bankName = account.bankName.getOrElse("") - new ModeratedAccountJSON( - account.accountId.value, - stringOptionOrNull(account.label), - stringOptionOrNull(account.number), - createOwnersJSON(account.owners.getOrElse(Set()), bankName), - stringOptionOrNull(account.accountType), - createAmountOfMoneyJSON(account.currency.getOrElse(""), account.balance), - stringOptionOrNull(account.iban), - stringOptionOrNull(account.swift_bic), - viewsAvailable, - stringOrNull(account.bankId.value) - ) - } - - def createTransactionsJSON(transactions: List[ModeratedTransaction]) : TransactionsJSON = { - new TransactionsJSON(transactions.map(createTransactionJSON)) - } - - def createTransactionJSON(transaction : ModeratedTransaction) : TransactionJSON = { - new TransactionJSON( - id = transaction.id.value, - this_account = transaction.bankAccount.map(createThisAccountJSON).getOrElse(null), - other_account = transaction.otherBankAccount.map(createOtherBankAccount).getOrElse(null), - details = createTransactionDetailsJSON(transaction), - metadata = transaction.metadata.map(createTransactionMetadataJSON).getOrElse(null) - ) - } - - def createTransactionCommentsJSON(comments : List[Comment]) : TransactionCommentsJSON = { - new TransactionCommentsJSON(comments.map(createTransactionCommentJSON)) - } - - def createTransactionCommentJSON(comment : Comment) : TransactionCommentJSON = { - new TransactionCommentJSON( - id = comment.id_, - value = comment.text, - date = comment.datePosted, - user = createUserJSON(comment.postedBy) - ) - } - - def createTransactionImagesJSON(images : List[TransactionImage]) : TransactionImagesJSON = { - new TransactionImagesJSON(images.map(createTransactionImageJSON)) - } - - def createTransactionImageJSON(image : TransactionImage) : TransactionImageJSON = { - new TransactionImageJSON( - id = image.id_, - label = image.description, - URL = image.imageUrl.toString, - date = image.datePosted, - user = createUserJSON(image.postedBy) - ) - } - - def createTransactionTagsJSON(tags : List[TransactionTag]) : TransactionTagsJSON = { - new TransactionTagsJSON(tags.map(createTransactionTagJSON)) - } - - def createTransactionTagJSON(tag : TransactionTag) : TransactionTagJSON = { - new TransactionTagJSON( - id = tag.id_, - value = tag.value, - date = tag.datePosted, - user = createUserJSON(tag.postedBy) - ) - } - - def createLocationJSON(loc : Option[GeoTag]) : LocationJSON = { - loc match { - case Some(location) => { - val user = createUserJSON(location.postedBy) - //test if the GeoTag is set to its default value - if(location.latitude == 0.0 & location.longitude == 0.0 & user == null) - null - else - new LocationJSON( - latitude = location.latitude, - longitude = location.longitude, - date = location.datePosted, - user = user - ) - } - case _ => null - } - } - - def createLocationPlainJSON(lat: Double, lon: Double) : LocationPlainJSON = { - new LocationPlainJSON( - latitude = lat, - longitude = lon - ) - } - - def createTransactionMetadataJSON(metadata : ModeratedTransactionMetadata) : TransactionMetadataJSON = { - new TransactionMetadataJSON( - narrative = stringOptionOrNull(metadata.ownerComment), - comments = metadata.comments.map(_.map(createTransactionCommentJSON)).getOrElse(null), - tags = metadata.tags.map(_.map(createTransactionTagJSON)).getOrElse(null), - images = metadata.images.map(_.map(createTransactionImageJSON)).getOrElse(null), - where = metadata.whereTag.map(createLocationJSON).getOrElse(null) - ) - } - - def createTransactionDetailsJSON(transaction : ModeratedTransaction) : TransactionDetailsJSON = { - new TransactionDetailsJSON( - `type` = stringOptionOrNull(transaction.transactionType), - description = stringOptionOrNull(transaction.description), - posted = transaction.startDate.getOrElse(null), - completed = transaction.finishDate.getOrElse(null), - new_balance = createAmountOfMoneyJSON(transaction.currency, transaction.balance), - value= createAmountOfMoneyJSON(transaction.currency, transaction.amount.map(_.toString)) - ) - } - - def createMinimalBankJSON(bankAccount : ModeratedBankAccount) : MinimalBankJSON = { - new MinimalBankJSON( - national_identifier = stringOptionOrNull(bankAccount.nationalIdentifier), - name = stringOptionOrNull(bankAccount.bankName) - ) - } - - def createMinimalBankJSON(bankAccount : ModeratedOtherBankAccount) : MinimalBankJSON = { - new MinimalBankJSON( - national_identifier = stringOptionOrNull(bankAccount.nationalIdentifier), - name = stringOptionOrNull(bankAccount.bankName) - ) - } - - def createThisAccountJSON(bankAccount : ModeratedBankAccount) : ThisAccountJSON = { - new ThisAccountJSON( - id = bankAccount.accountId.value, - number = stringOptionOrNull(bankAccount.number), - kind = stringOptionOrNull(bankAccount.accountType), - IBAN = stringOptionOrNull(bankAccount.iban), - swift_bic = stringOptionOrNull(bankAccount.swift_bic), - bank = createMinimalBankJSON(bankAccount), - holders = bankAccount.owners.map(x => x.toList.map(h => createAccountHolderJSON(h, false))).getOrElse(null) - ) - } - - def createAccountHolderJSON(owner : User, isAlias : Boolean) : AccountHolderJSON = { - new AccountHolderJSON( - name = owner.name, - is_alias = isAlias - ) - } - - def createAccountHolderJSON(name : String, isAlias : Boolean) : AccountHolderJSON = { - new AccountHolderJSON( - name = name, - is_alias = isAlias - ) - } - - def createOtherAccountMetaDataJSON(metadata : ModeratedOtherBankAccountMetadata) : OtherAccountMetadataJSON = { - new OtherAccountMetadataJSON( - public_alias = stringOptionOrNull(metadata.publicAlias), - private_alias = stringOptionOrNull(metadata.privateAlias), - more_info = stringOptionOrNull(metadata.moreInfo), - URL = stringOptionOrNull(metadata.url), - image_URL = stringOptionOrNull(metadata.imageURL), - open_corporates_URL = stringOptionOrNull(metadata.openCorporatesURL), - corporate_location = metadata.corporateLocation.map(createLocationJSON).getOrElse(null), - physical_location = metadata.physicalLocation.map(createLocationJSON).getOrElse(null) - ) - } - - def createOtherBankAccount(bankAccount : ModeratedOtherBankAccount) : OtherAccountJSON = { - new OtherAccountJSON( - id = bankAccount.id, - number = stringOptionOrNull(bankAccount.number), - kind = stringOptionOrNull(bankAccount.kind), - IBAN = stringOptionOrNull(bankAccount.iban), - swift_bic = stringOptionOrNull(bankAccount.swift_bic), - bank = createMinimalBankJSON(bankAccount), - holder = createAccountHolderJSON(bankAccount.label.display, bankAccount.isAlias), - metadata = bankAccount.metadata.map(createOtherAccountMetaDataJSON).getOrElse(null) - ) - } - - def createOtherBankAccountsJSON(otherBankAccounts : List[ModeratedOtherBankAccount]) : OtherAccountsJSON = { - val otherAccountsJSON : List[OtherAccountJSON] = otherBankAccounts.map(createOtherBankAccount) - OtherAccountsJSON(otherAccountsJSON) - } - - def createUserJSON(user : User) : UserJSON = { - new UserJSON( - user.idGivenByProvider, - stringOrNull(user.provider), - stringOrNull(user.emailAddress) //TODO: shouldn't this be the display name? - ) - } - - def createUserJSON(user : Box[User]) : UserJSON = { - user match { - case Full(u) => createUserJSON(u) - case _ => null - } - } - - def createOwnersJSON(owners : Set[User], bankName : String) : List[UserJSON] = { - owners.map(o => { - new UserJSON( - o.idGivenByProvider, - stringOrNull(o.provider), - stringOrNull(o.name) - ) - } - ).toList - } - - def createAmountOfMoneyJSON(currency : String, amount : String) : AmountOfMoneyJSON = { - new AmountOfMoneyJSON( - stringOrNull(currency), - stringOrNull(amount) - ) - } - - def createAmountOfMoneyJSON(currency : Option[String], amount : Option[String]) : AmountOfMoneyJSON = { - new AmountOfMoneyJSON( - stringOptionOrNull(currency), - stringOptionOrNull(amount) - ) - } - - def createAmountOfMoneyJSON(currency : Option[String], amount : String) : AmountOfMoneyJSON = { - new AmountOfMoneyJSON( - stringOptionOrNull(currency), - stringOrNull(amount) - ) - } - - def createPermissionsJSON(permissions : List[Permission]) : PermissionsJSON = { - val permissionsJson = permissions.map(p => { - new PermissionJSON( - createUserJSON(p.user), - p.views.map(createViewJSON) - ) - }) - new PermissionsJSON(permissionsJson) - } - - def createAliasJSON(alias: String): AliasJSON = { - AliasJSON(stringOrNull(alias)) - } - - def createTransactionNarrativeJSON(narrative: String): TransactionNarrativeJSON = { - TransactionNarrativeJSON(stringOrNull(narrative)) - } } \ No newline at end of file From 84b38c5d5637f6c11ed92b26d8fa2c3251989a06 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 15 Jan 2016 18:37:06 +0100 Subject: [PATCH 280/702] Added external user via Kafka functionality --- src/main/scala/bootstrap/liftweb/Boot.scala | 9 +- .../scala/code/model/dataAccess/OBPUser.scala | 139 +++++++++++++++++- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index b79615e57..e9f384f6d 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -158,7 +158,8 @@ class Boot extends Loggable{ } // ensure our relational database's tables are created/fit the schema - if(Props.get("connector").getOrElse("") == "mapped") + if(Props.get("connector").getOrElse("") == "mapped" || + Props.get("connector").getOrElse("") == "kafka" ) schemifyAll() // This sets up MongoDB config (for the mongodb connector) @@ -284,7 +285,11 @@ class Boot extends Loggable{ val useMessageQueue = Props.getBool("messageQueue.createBankAccounts", false) if(useMessageQueue) - BankAccountCreationListener.startListen + try { + BankAccountCreationListener.startListen + } catch { + case e: java.lang.ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e") + } Mailer.devModeSend.default.set( (m : MimeMessage) => { logger.info("Would have sent email if not in dev mode: " + m.getContent) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 769ee3566..96ffa497e 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -35,10 +35,13 @@ import net.liftweb.mapper._ import net.liftweb.util.Mailer.{BCC, To, Subject, From} import net.liftweb.util._ import net.liftweb.common._ -import scala.xml.NodeSeq +import scala.xml.{Text, NodeSeq} import net.liftweb.http.{SHtml, SessionVar, Templates, S} import net.liftweb.http.js.JsCmds.FocusOnLoad +import java.util.UUID +import code.bankconnectors.{KafkaProducer, KafkaConsumer} +import code.sandbox.SandboxUserImport /** * An O-R mapped "User" class that includes first name, last name, password @@ -48,6 +51,15 @@ class OBPUser extends MegaProtoUser[OBPUser] with Logger { object user extends MappedLongForeignKey(this, APIUser) + /** + * The provider field for the User. + */ + lazy val provider: userProvider = new userProvider() + class userProvider extends MappedString(this, 64) { + override def displayName = S.?("provider") + override val fieldId = Some(Text("txtProvider")) + } + def displayName() = { if(firstName.get.isEmpty) { lastName.get @@ -58,11 +70,21 @@ class OBPUser extends MegaProtoUser[OBPUser] with Logger { } } + def getProvider() = { + if(provider.get == null) { + Props.get("hostname","") + } else if ( provider.get == "" || provider.get == Props.get("hostname","") ) { + Props.get("hostname","") + } else { + provider.get + } + } + def createUnsavedApiUser() : APIUser = { APIUser.create .name_(displayName()) .email(email) - .provider_(Props.get("hostname","")) + .provider_(getProvider()) .providerId(email) } @@ -104,7 +126,7 @@ import net.liftweb.util.Helpers._ override def screenWrap = Full() // define the order fields will appear in forms and output - override def fieldOrder = List(id, firstName, lastName, email, password) + override def fieldOrder = List(id, firstName, lastName, email, password, provider) override def signupFields = List(firstName, lastName, email, password) // comment this line out to require email validations @@ -230,12 +252,54 @@ import net.liftweb.util.Helpers._
    } + def getUserViaKafka( username: String, password: String ): Box[SandboxUserImport] = { + // Generate random uuid to be used as request-respose match id + val reqId: String = UUID.randomUUID().toString + + // Create Kafka producer, using list of brokers from Zookeeper + val producer: KafkaProducer = new KafkaProducer() + // Send request to Kafka, marked with reqId + // so we can fetch the corresponding response + val argList = Map( "email" -> username, + "password" -> password ) + producer.send(reqId, "getUser", argList, "1") + + // Request sent, now we wait for response with the same reqId + val consumer = new KafkaConsumer() + // Create entry only for the first item on returned list + val r = consumer.getResponse(reqId).head + + // For testing without Kafka + //val r = Map("email"->"test@email.me","password"->"secret","display_name"->"DN") + + var recDisplayName = r.getOrElse("display_name", "Not Found") + var recEmail = r.getOrElse("email", "Not Found") + + if (recEmail == username && recEmail != "Not Found") { + if (recDisplayName == "") + Full(new SandboxUserImport( username, password, recEmail)) + else + Full(new SandboxUserImport( username, password, recDisplayName)) + } else { + // If empty result from Kafka return empty data + Empty + } + } + + def userLoginFailed = { + info("failed: " + failedLoginRedirect.get) + failedLoginRedirect.get.foreach(S.redirectTo(_)) + S.error("login", S.?("Invalid Username or Password")) + } + //overridden to allow a redirection if login fails override def login = { if (S.post_?) { S.param("username"). flatMap(username => findUserByUserName(username)) match { case Full(user) if user.validated_? && + // Check if user came from localhost + user.getProvider() == Props.get("hostname","") && user.testPassword(S.param("password")) => { val preLoginState = capturePreLoginState() info("login redir: " + loginRedirect.get) @@ -260,9 +324,70 @@ import net.liftweb.util.Helpers._ S.error(S.?("account.validation.error")) case _ => { - info("failed: " + failedLoginRedirect.get) - failedLoginRedirect.get.foreach(S.redirectTo(_)) - S.error("login", S.?("Invalid Username or Password")) + // If not found locally, try to authenticate user via Kafka, if enabled in props + if (Props.get("connector").openOrThrowException("no connector set") == "kafka") { + S.param("username"). + flatMap(username => getUserViaKafka(username, S.param("password").openOr(""))) match { + case Full(SandboxUserImport(extEmail, extPassword, extDisplayName)) => { + val preLoginState = capturePreLoginState() + info("external user authenticated. login redir: " + loginRedirect.get) + val redir = loginRedirect.get match { + case Full(url) => + loginRedirect(Empty) + url + case _ => + homePage + } + + val dummyPassword = "nothingreallyjustdummypass" + val extProvider = Props.get("connector").openOrThrowException("no connector set") + + val user = S.param("username"). + flatMap(username => findUserByUserName(username)) match { + + // Check if the external user is already created locally + case Full(user) if user.validated_? && + user.provider == extProvider => { + // Return existing user if found + info("external user already exists locally, using that one") + user + } + + // If not found, create new user + case _ => { + // Create OBPUser using fetched data from Kafka + // assuming that user's email is always validated + info("external user does not exist locally, creating one") + val newUser = OBPUser.create + .firstName(extDisplayName) + .email(extEmail) + // No need to store password, so store dummy string instead + .password(dummyPassword) + .provider(extProvider) + .validated(true) + // Save the user in order to be able to log in + newUser.save() + // Return created user + newUser + } + } + + logUserIn(user, () => { + S.notice(S.?("logged.in")) + + preLoginState() + + S.redirectTo(homePage) + }) + + } + case _ => { + userLoginFailed + } + } + } else { + userLoginFailed + } } } } @@ -311,4 +436,4 @@ import net.liftweb.util.Helpers._ innerSignup } -} \ No newline at end of file +} From 0b78f44ba3d853b1ae135387de1a1b95686b3ce3 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 15 Jan 2016 19:10:19 +0100 Subject: [PATCH 281/702] Code cleanup --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index e9f384f6d..8d3f71ca5 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -283,7 +283,7 @@ class Boot extends Loggable{ // Make a transaction span the whole HTTP request S.addAround(DB.buildLoanWrapper) - val useMessageQueue = Props.getBool("messageQueue.createBankAccounts", false) + val useMessageQueue = Props.getBool("messageQueue.createBankAccounts") openOr false if(useMessageQueue) try { BankAccountCreationListener.startListen From 86967fa28c34e38f38b5c2cd0f723d0aadd2fb51 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 15 Jan 2016 19:52:28 +0100 Subject: [PATCH 282/702] Fixed null exception in Boot.scala --- src/main/scala/bootstrap/liftweb/Boot.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 8d3f71ca5..26389df65 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -283,13 +283,13 @@ class Boot extends Loggable{ // Make a transaction span the whole HTTP request S.addAround(DB.buildLoanWrapper) - val useMessageQueue = Props.getBool("messageQueue.createBankAccounts") openOr false - if(useMessageQueue) - try { + try { + val useMessageQueue = Props.getBool("messageQueue.createBankAccounts", false) + if(useMessageQueue) BankAccountCreationListener.startListen - } catch { - case e: java.lang.ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e") - } + } catch { + case e: java.lang.ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e") + } Mailer.devModeSend.default.set( (m : MimeMessage) => { logger.info("Would have sent email if not in dev mode: " + m.getContent) From 02ea1e1d7caaeeae0d7b1ad2938578b008b004d2 Mon Sep 17 00:00:00 2001 From: florind Date: Sun, 17 Jan 2016 14:12:21 +0100 Subject: [PATCH 283/702] Default log4j.properties file added, INFO-level logging. --- src/main/resources/log4j.properties | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/resources/log4j.properties diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 000000000..5e05a47db --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootCategory=INFO, CONSOLE + +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=%d [%t] %-5p %c %x - %m%n From 1335b51881438c4f660f3d387e83fb2d71e7f80d Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 17 Jan 2016 14:23:50 +0100 Subject: [PATCH 284/702] Adding partialFunction to ResourceDoc so we can test for existance in API routes --- src/main/scala/code/api/util/APIUtil.scala | 6 +- .../scala/code/api/v1_2_1/APIMethods121.scala | 71 +++++++++++++++ .../scala/code/api/v1_3_0/APIMethods130.scala | 2 + .../scala/code/api/v1_4_0/APIMethods140.scala | 89 +++++++++++-------- .../scala/code/api/v2_0_0/APIMethods200.scala | 1 + 5 files changed, 130 insertions(+), 39 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 2799c9f8c..71511d312 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -34,8 +34,9 @@ package code.api.util import code.api.v1_2.ErrorMessage import code.metrics.APIMetrics -import net.liftweb.common.{Full, Loggable} -import net.liftweb.http.{JsonResponse, S} +import code.model.User +import net.liftweb.common.{Box, Full, Loggable} +import net.liftweb.http.{Req, JsonResponse, S} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.js.JsExp import net.liftweb.json.Extraction @@ -264,6 +265,7 @@ object APIUtil extends Loggable { Used to document API calls / resources. correct place for this? */ case class ResourceDoc( + partialFunction : PartialFunction[Req, Box[User] => Box[JsonResponse]], apiVersion: String, // TODO: Constrain to certain strings? apiFunction: String, // The partial function that implements this resource. Could use it to link to the source code that implements the call requestVerb: String, // GET, POST etc. TODO: Constrain to GET, POST etc. diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index e581d08b4..a72c77b73 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -75,6 +75,7 @@ trait APIMethods121 { resourceDocs += ResourceDoc( + root(apiVersion), apiVersion, "root", "GET", @@ -104,6 +105,7 @@ trait APIMethods121 { resourceDocs += ResourceDoc( + allBanks, apiVersion, "allBanks", "GET", @@ -137,6 +139,7 @@ trait APIMethods121 { resourceDocs += ResourceDoc( + bankById, apiVersion, "bankById", "GET", @@ -167,6 +170,7 @@ trait APIMethods121 { resourceDocs += ResourceDoc( + allAccountsAllBanks, apiVersion, "allAccountsAllBanks", "GET", @@ -196,6 +200,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + privateAccountsAllBanks, apiVersion, "privateAccountsAllBanks", "GET", @@ -223,6 +228,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + publicAccountsAllBanks, apiVersion, "publicAccountsAllBanks", "GET", @@ -244,6 +250,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + allAccountsAtOneBank, apiVersion, "allAccountsAtOneBank", "GET", @@ -276,6 +283,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + privateAccountsAtOneBank, apiVersion, "privateAccountsAtOneBank", "GET", @@ -304,6 +312,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + publicAccountsAtOneBank, apiVersion, "publicAccountsAtOneBank", "GET", @@ -330,6 +339,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + accountById, apiVersion, "accountById", "GET", @@ -369,6 +379,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateAccountLabel, apiVersion, "updateAccountLabel", "POST", @@ -396,6 +407,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getViewsForBankAccount, apiVersion, "getViewsForBankAccount", "GET", @@ -446,6 +458,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + createViewForBankAccount, apiVersion, "createViewForBankAccount", "POST", @@ -485,6 +498,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateViewForBankAccount, apiVersion, "updateViewForBankAccount", "PUT", @@ -517,6 +531,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteViewForBankAccount, apiVersion, "deleteViewForBankAccount", "DELETE", @@ -540,6 +555,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getPermissionsForBankAccount, apiVersion, "getPermissionsForBankAccount", "GET", @@ -568,6 +584,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getPermissionForUserForBankAccount, apiVersion, "getPermissionForUserForBankAccount", "GET", @@ -597,6 +614,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + addPermissionForUserForBankAccountForMultipleViews, apiVersion, "addPermissionForUserForBankAccountForMultipleViews", "POST", @@ -628,6 +646,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + addPermissionForUserForBankAccountForOneView, apiVersion, "addPermissionForUserForBankAccountForOneView", "POST", @@ -659,6 +678,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + removePermissionForUserForBankAccountForOneView, apiVersion, "removePermissionForUserForBankAccountForOneView", "DELETE", @@ -687,6 +707,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + removePermissionForUserForBankAccountForAllViews, apiVersion, "removePermissionForUserForBankAccountForAllViews", "DELETE", @@ -713,6 +734,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getCounterpartiesForBankAccount, apiVersion, "getCounterpartiesForBankAccount", "GET", @@ -741,6 +763,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getCounterpartyByIdForBankAccount, apiVersion, "getCounterpartyByIdForBankAccount", "GET", @@ -769,6 +792,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getCounterpartyMetadata, apiVersion, "getCounterpartyMetadata", "GET", @@ -798,6 +822,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getCounterpartyPublicAlias, apiVersion, "getCounterpartyPublicAlias", "GET", @@ -828,6 +853,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + addCounterpartyPublicAlias, apiVersion, "addCounterpartyPublicAlias", "POST", @@ -864,6 +890,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyPublicAlias, apiVersion, "updateCounterpartyPublicAlias", "PUT", @@ -895,6 +922,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyPublicAlias, apiVersion, "deleteCounterpartyPublicAlias", "DELETE", @@ -923,6 +951,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getCounterpartyPrivateAlias, apiVersion, "getCounterpartyPrivateAlias", "GET", @@ -953,6 +982,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + addCounterpartyPrivateAlias, apiVersion, "addCounterpartyPrivateAlias", "POST", @@ -985,6 +1015,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyPrivateAlias, apiVersion, "updateCounterpartyPrivateAlias", "PUT", @@ -1017,6 +1048,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyPrivateAlias, apiVersion, "deleteCounterpartyPrivateAlias", "DELETE", @@ -1047,6 +1079,7 @@ trait APIMethods121 { //TODO: get more info of counterparty? resourceDocs += ResourceDoc( + addCounterpartyMoreInfo, apiVersion, "addCounterpartyMoreInfo", "POST", @@ -1077,6 +1110,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyMoreInfo, apiVersion, "updateCounterpartyMoreInfo", "PUT", @@ -1107,6 +1141,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyMoreInfo, apiVersion, "deleteCounterpartyMoreInfo", "DELETE", @@ -1135,6 +1170,7 @@ trait APIMethods121 { //TODO: get url of counterparty? resourceDocs += ResourceDoc( + addCounterpartyUrl, apiVersion, "addCounterpartyUrl", "POST", @@ -1166,6 +1202,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyUrl, apiVersion, "updateCounterpartyUrl", "PUT", @@ -1196,6 +1233,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyUrl, apiVersion, "deleteCounterpartyUrl", "DELETE", @@ -1224,6 +1262,7 @@ trait APIMethods121 { //TODO: get image url of counterparty? resourceDocs += ResourceDoc( + addCounterpartyImageUrl, apiVersion, "addCounterpartyImageUrl", "POST", @@ -1254,6 +1293,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyImageUrl, apiVersion, "updateCounterpartyImageUrl", "PUT", @@ -1284,6 +1324,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyImageUrl, apiVersion, "deleteCounterpartyImageUrl", "DELETE", @@ -1312,6 +1353,7 @@ trait APIMethods121 { //TODO: get open corporates url of counterparty? resourceDocs += ResourceDoc( + addCounterpartyOpenCorporatesUrl, apiVersion, "addCounterpartyOpenCorporatesUrl", "POST", @@ -1342,6 +1384,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyOpenCorporatesUrl, apiVersion, "updateCounterpartyOpenCorporatesUrl", "PUT", @@ -1372,6 +1415,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyOpenCorporatesUrl, apiVersion, "deleteCounterpartyOpenCorporatesUrl", "DELETE", @@ -1400,6 +1444,7 @@ trait APIMethods121 { //TODO: get corporate location of counterparty? resourceDocs += ResourceDoc( + addCounterpartyCorporateLocation, apiVersion, "addCounterpartyCorporateLocation", "POST", @@ -1432,6 +1477,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyCorporateLocation, apiVersion, "updateCounterpartyCorporateLocation", "PUT", @@ -1464,6 +1510,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyCorporateLocation, apiVersion, "deleteCounterpartyCorporateLocation", "DELETE", @@ -1497,6 +1544,7 @@ trait APIMethods121 { //TODO: get physical location of counterparty? resourceDocs += ResourceDoc( + addCounterpartyPhysicalLocation, apiVersion, "addCounterpartyPhysicalLocation", "POST", @@ -1529,6 +1577,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateCounterpartyPhysicalLocation, apiVersion, "updateCounterpartyPhysicalLocation", "PUT", @@ -1561,6 +1610,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteCounterpartyPhysicalLocation, apiVersion, "deleteCounterpartyPhysicalLocation", "DELETE", @@ -1592,6 +1642,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getTransactionsForBankAccount, apiVersion, "getTransactionsForBankAccount", "GET", @@ -1633,6 +1684,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getTransactionByIdForBankAccount, apiVersion, "getTransactionByIdForBankAccount", "GET", @@ -1661,6 +1713,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getTransactionNarrative, apiVersion, "getTransactionNarrative", "GET", @@ -1688,6 +1741,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + addTransactionNarrative, apiVersion, "addTransactionNarrative", "POST", @@ -1718,6 +1772,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + updateTransactionNarrative, apiVersion, "updateTransactionNarrative", "PUT", @@ -1748,6 +1803,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + deleteTransactionNarrative, apiVersion, "deleteTransactionNarrative", "DELETE", @@ -1775,6 +1831,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getCommentsForViewOnTransaction, apiVersion, "getCommentsForViewOnTransaction", "GET", @@ -1802,6 +1859,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + addCommentForViewOnTransaction, apiVersion, "addCommentForViewOnTransaction", "POST", @@ -1833,6 +1891,7 @@ trait APIMethods121 { // Not able to update a comment (delete and add another) resourceDocs += ResourceDoc( + deleteCommentForViewOnTransaction, apiVersion, "deleteCommentForViewOnTransaction", "DELETE", @@ -1860,6 +1919,7 @@ trait APIMethods121 { } resourceDocs += ResourceDoc( + getTagsForViewOnTransaction, apiVersion, "getTagsForViewOnTransaction", "GET", @@ -1887,6 +1947,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + addTagForViewOnTransaction, apiVersion, "addTagForViewOnTransaction", "POST", @@ -1919,6 +1980,7 @@ Authentication via OAuth is required if the view is not public.""", // No update tag (delete and add another) resourceDocs += ResourceDoc( + deleteTagForViewOnTransaction, apiVersion, "deleteTagForViewOnTransaction", "DELETE", @@ -1947,6 +2009,7 @@ Authentication via OAuth is required. The user must either have owner privileges } resourceDocs += ResourceDoc( + getImagesForViewOnTransaction, apiVersion, "getImagesForViewOnTransaction", "GET", @@ -1974,6 +2037,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + addImageForViewOnTransaction, apiVersion, "addImageForViewOnTransaction", "POST", @@ -2004,6 +2068,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + deleteImageForViewOnTransaction, apiVersion, "deleteImageForViewOnTransaction", "DELETE", @@ -2031,6 +2096,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + getWhereTagForViewOnTransaction, apiVersion, "getWhereTagForViewOnTransaction", "GET", @@ -2060,6 +2126,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + addWhereTagForViewOnTransaction, apiVersion, "addWhereTagForViewOnTransaction", "POST", @@ -2092,6 +2159,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + updateWhereTagForViewOnTransaction, apiVersion, "updateWhereTagForViewOnTransaction", "PUT", @@ -2124,6 +2192,7 @@ Authentication via OAuth is required if the view is not public.""", } resourceDocs += ResourceDoc( + deleteWhereTagForViewOnTransaction, apiVersion, "deleteWhereTagForViewOnTransaction", "DELETE", @@ -2155,6 +2224,7 @@ Authentication via OAuth is required. The user must either have owner privileges } resourceDocs += ResourceDoc( + getCounterpartyForTransaction, apiVersion, "getCounterpartyForTransaction", "GET", @@ -2187,6 +2257,7 @@ Authentication via OAuth is required if the view is not public.""", case class TransactionIdJson(transaction_id : String) resourceDocs += ResourceDoc( + makePayment, apiVersion, "makePayment", "POST", diff --git a/src/main/scala/code/api/v1_3_0/APIMethods130.scala b/src/main/scala/code/api/v1_3_0/APIMethods130.scala index 2769f93db..6ef7e53ad 100644 --- a/src/main/scala/code/api/v1_3_0/APIMethods130.scala +++ b/src/main/scala/code/api/v1_3_0/APIMethods130.scala @@ -30,6 +30,7 @@ trait APIMethods130 { resourceDocs += ResourceDoc( + getCards, apiVersion, "getCards", "GET", @@ -58,6 +59,7 @@ trait APIMethods130 { resourceDocs += ResourceDoc( + getCardsForBank, apiVersion, "getCardsForBank", "GET", diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 230a0dac4..6de369e12 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -92,6 +92,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } resourceDocs += ResourceDoc( + getCustomer, apiVersion, "getCustomer", "GET", @@ -120,6 +121,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } resourceDocs += ResourceDoc( + getCustomerMessages, apiVersion, "getCustomerMessages", "GET", @@ -150,6 +152,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } resourceDocs += ResourceDoc( + addCustomerMessage, apiVersion, "addCustomerMessage", "POST", @@ -182,6 +185,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } resourceDocs += ResourceDoc( + getBranches, apiVersion, "getBranches", "GET", @@ -220,6 +224,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with resourceDocs += ResourceDoc( + getAtms, apiVersion, "getAtms", "GET", @@ -257,6 +262,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with resourceDocs += ResourceDoc( + getProducts, apiVersion, "getProducts", "GET", @@ -298,6 +304,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with resourceDocs += ResourceDoc( + getCrmEvents, apiVersion, "getCrmEvents", "GET", @@ -328,6 +335,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } resourceDocs += ResourceDoc( + getResourceDocsObp(apiVersion), apiVersion, "getResourceDocsObp", "GET", @@ -369,6 +377,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with resourceDocs += ResourceDoc( + getResourceDocsSwagger(apiVersion), apiVersion, "getResourceDocsSwagger", "GET", @@ -404,6 +413,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with */ resourceDocs += ResourceDoc( + getTransactionRequestTypes, apiVersion, "getTransactionRequestTypes", "GET", @@ -436,6 +446,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } resourceDocs += ResourceDoc( + getTransactionRequests, apiVersion, "getTransactionRequests", "GET", @@ -472,6 +483,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with case class TransactionIdJson(transaction_id : String) resourceDocs += ResourceDoc( + createTransactionRequest, apiVersion, "createTransactionRequest", "POST", @@ -523,6 +535,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with resourceDocs += ResourceDoc( + answerTransactionRequestChallenge, apiVersion, "answerTransactionRequestChallenge", "POST", @@ -562,6 +575,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with resourceDocs += ResourceDoc( + addCustomer, apiVersion, "addCustomer", "POST", @@ -602,42 +616,43 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with - if (Props.devMode) { - resourceDocs += ResourceDoc( - apiVersion, - "testResourceDoc", - "GET", - "/i-do-not-exist-i-will-404", - "I am only a test resource Doc", - """ - | - |#This should be H1 - | - |##This should be H2 - | - |###This should be H3 - | - |####This should be H4 - | - |Here is a list with two items: - | - |* One - |* Two - | - |There are underscores by them selves _ - | - |There are _underscores_ around a word - | - |There are underscores_in_words - | - |There are 'underscores_in_words_inside_quotes' - | - |There are (underscores_in_words_in_brackets) - | - |_etc_...""", - emptyObjectJson, - emptyObjectJson, - emptyObjectJson :: Nil) - } +// if (Props.devMode) { +// resourceDocs += ResourceDoc( +// what-to-use-here?, +// apiVersion, +// "testResourceDoc", +// "GET", +// "/i-do-not-exist-i-will-404", +// "I am only a test resource Doc", +// """ +// | +// |#This should be H1 +// | +// |##This should be H2 +// | +// |###This should be H3 +// | +// |####This should be H4 +// | +// |Here is a list with two items: +// | +// |* One +// |* Two +// | +// |There are underscores by them selves _ +// | +// |There are _underscores_ around a word +// | +// |There are underscores_in_words +// | +// |There are 'underscores_in_words_inside_quotes' +// | +// |There are (underscores_in_words_in_brackets) +// | +// |_etc_...""", +// emptyObjectJson, +// emptyObjectJson, +// emptyObjectJson :: Nil) +// } } } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 41bdb87e7..7b63c41e1 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -59,6 +59,7 @@ trait APIMethods200 { val apiVersion : String = "2_0_0" resourceDocs += ResourceDoc( + allAccountsAllBanks, apiVersion, "allAccountsAllBanks", "GET", From 431bcb842862014b543080ddb084910be9952010 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 17 Jan 2016 16:07:36 +0100 Subject: [PATCH 285/702] getResourceDocsList returns APIs that are present in version routes --- src/main/scala/code/api/util/APIUtil.scala | 5 +- .../scala/code/api/v1_4_0/APIMethods140.scala | 136 ++++++++++-------- 2 files changed, 79 insertions(+), 62 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 71511d312..af9b4347c 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -262,7 +262,10 @@ object APIUtil extends Loggable { } /* - Used to document API calls / resources. correct place for this? + Used to document API calls / resources. + + TODO Can we extract apiVersion, apiFunction, requestVerb and requestUrl from partialFunction? + */ case class ResourceDoc( partialFunction : PartialFunction[Req, Box[User] => Box[JsonResponse]], diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 6de369e12..d8add8b26 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -22,9 +22,11 @@ import scala.collection.immutable.Nil import collection.mutable.ArrayBuffer import code.api.APIFailure -import code.api.v1_2_1.APIMethods121 -import code.api.v1_3_0.APIMethods130 -import code.api.v2_0_0.APIMethods200 // So we can include resource docs from future versions +import code.api.v1_2_1.{OBPAPI1_2_1, APIInfoJSON, HostedBy, APIMethods121} +import code.api.v1_3_0.{OBPAPI1_3_0, APIMethods130} +import code.api.v2_0_0.{OBPAPI2_0_0, APIMethods200} + +// So we can include resource docs from future versions import code.api.v1_4_0.JSONFactory1_4_0._ import code.atms.Atms import code.branches.Branches @@ -55,39 +57,35 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with val exampleDate = simpleDateFormat.parse(exampleDateString) - - def getResourceDocsList(requestedApiVersion : String) : Option[List[ResourceDoc]] = { - // We'll have a different list of resource docs depending on the version being called. - // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) - - val path = CurrentReq.value.path - - logger.info(s"path is $path") - - val activeVersion = CurrentReq.value.path(1) - - logger.info(s"ver in path is $activeVersion") + // Return a different list of resource docs depending on the version being called. + // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) etc. val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + // When we add a new version, update this. + val resourceDocsAll = resourceDocs2_0_0 + val cumulativeResourceDocs = requestedApiVersion match { case "2.0.0" => resourceDocs2_0_0 case "1.4.0" => resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.2.1" => Implementations1_2_1.resourceDocs - case _ => resourceDocs2_0_0 // Not sure if we should have a default here. + case _ => resourceDocsAll } - // TODO Filter out non available APIs for the active version - // e.g. For each version we should only include those resourceDocs that are listed in the routes List - // (e.g. OBPAPI1_2_1 routes) unless we are in dev mode (Props.devMode is true) otherwise developers will see a huge list of APIs and get a bunch of 404s + // Only return APIs that are present in the list of routes for the version called + val liveResourceDocs = requestedApiVersion match { + case "2.0.0" => cumulativeResourceDocs.filter(rd => OBPAPI2_0_0.routes.contains(rd.partialFunction)) + case "1.4.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_4_0.routes.contains(rd.partialFunction)) + case "1.3.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_3_0.routes.contains(rd.partialFunction)) + case "1.2.1" => cumulativeResourceDocs.filter(rd => OBPAPI1_2_1.routes.contains(rd.partialFunction)) + case _ => cumulativeResourceDocs // Will be all ResourceDocs across all versions + } - - - // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. + // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. Some(cumulativeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) } @@ -359,7 +357,7 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with ) // Provides resource documents so that API Explorer (or other apps) can display API documentation - // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple various of markdown. + // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown. def getResourceDocsObp(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: "obp" :: Nil JsonGet _ => { user => { @@ -616,43 +614,59 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with -// if (Props.devMode) { -// resourceDocs += ResourceDoc( -// what-to-use-here?, -// apiVersion, -// "testResourceDoc", -// "GET", -// "/i-do-not-exist-i-will-404", -// "I am only a test resource Doc", -// """ -// | -// |#This should be H1 -// | -// |##This should be H2 -// | -// |###This should be H3 -// | -// |####This should be H4 -// | -// |Here is a list with two items: -// | -// |* One -// |* Two -// | -// |There are underscores by them selves _ -// | -// |There are _underscores_ around a word -// | -// |There are underscores_in_words -// | -// |There are 'underscores_in_words_inside_quotes' -// | -// |There are (underscores_in_words_in_brackets) -// | -// |_etc_...""", -// emptyObjectJson, -// emptyObjectJson, -// emptyObjectJson :: Nil) -// } + if (Props.devMode) { + resourceDocs += ResourceDoc( + dummy(apiVersion), + apiVersion, + "testResourceDoc", + "GET", + "/dummy", + "I am only a test resource Doc", + """ + | + |#This should be H1 + | + |##This should be H2 + | + |###This should be H3 + | + |####This should be H4 + | + |Here is a list with two items: + | + |* One + |* Two + | + |There are underscores by them selves _ + | + |There are _underscores_ around a word + | + |There are underscores_in_words + | + |There are 'underscores_in_words_inside_quotes' + | + |There are (underscores_in_words_in_brackets) + | + |_etc_...""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + } + + + + def dummy(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "dummy" :: Nil JsonGet json => { + user => + val apiDetails: JValue = { + val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994") + val apiInfoJSON = new APIInfoJSON(apiVersion, gitCommit, hostedBy) + Extraction.decompose(apiInfoJSON) + } + + Full(successJsonResponse(apiDetails, 200)) + } + } + } } From 7c7387ba621521431d21f0e3c0345667d8929792 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 18 Jan 2016 08:45:22 +0100 Subject: [PATCH 286/702] Moving ResourceDocs to its own package (to fix circular dependency) --- src/main/scala/bootstrap/liftweb/Boot.scala | 10 + .../code/api/ResourceDocs/ResourceDocs.scala | 22 ++ .../ResourceDocs/ResourceDocsAPIMethods.scala | 214 ++++++++++++++++++ .../SwaggerJSONFactory1_4_0.scala | 5 +- .../scala/code/api/v1_2_1/OBPAPI1.2.1.scala | 10 +- .../scala/code/api/v1_3_0/OBPAPI1_3_0.scala | 10 +- .../scala/code/api/v1_4_0/APIMethods140.scala | 114 +--------- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 1 + .../scala/code/api/v1_4_0/OBPAPI1_4_0.scala | 2 - .../scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 6 +- 10 files changed, 263 insertions(+), 131 deletions(-) create mode 100644 src/main/scala/code/api/ResourceDocs/ResourceDocs.scala create mode 100644 src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala rename src/main/scala/code/api/{v1_4_0 => ResourceDocs}/SwaggerJSONFactory1_4_0.scala (99%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 26389df65..addfb2b85 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -31,6 +31,7 @@ Berlin 13359, Germany */ package bootstrap.liftweb +import code.api.ResourceDocs.ResourceDocs import code.api.sandbox.SandboxApiCalls import code.crm.MappedCrmEvent import code.management.ImporterAPI @@ -66,6 +67,9 @@ import code.model._ import code.model.dataAccess._ import code.api._ import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} +import code.api.ResourceDocs.{ResourceDocsAPIMethods} + + /** * A class that's instantiated early and run. It allows the application @@ -186,6 +190,7 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(v1_0.OBPAPI1_0) LiftRules.statelessDispatch.append(v1_1.OBPAPI1_1) LiftRules.statelessDispatch.append(v1_2.OBPAPI1_2) + // Can we depreciate the above? LiftRules.statelessDispatch.append(v1_2_1.OBPAPI1_2_1) LiftRules.statelessDispatch.append(v1_3_0.OBPAPI1_3_0) LiftRules.statelessDispatch.append(v1_4_0.OBPAPI1_4_0) @@ -199,6 +204,11 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(CashAccountAPI) LiftRules.statelessDispatch.append(BankMockAPI) + + + // Add Resource Docs + LiftRules.statelessDispatch.append(ResourceDocs) + // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below //add sandbox api calls only if we're running in sandbox mode diff --git a/src/main/scala/code/api/ResourceDocs/ResourceDocs.scala b/src/main/scala/code/api/ResourceDocs/ResourceDocs.scala new file mode 100644 index 000000000..fc250481a --- /dev/null +++ b/src/main/scala/code/api/ResourceDocs/ResourceDocs.scala @@ -0,0 +1,22 @@ +package code.api.ResourceDocs + +import code.api.OBPRestHelper + + +import net.liftweb.common.Loggable + + +object ResourceDocs extends OBPRestHelper with ResourceDocsAPIMethods with Loggable { + + val VERSION = "1.3.0" + + val routes = List( + ImplementationsResourceDocs.getResourceDocsObp(VERSION), + ImplementationsResourceDocs.getResourceDocsSwagger(VERSION) + ) + + routes.foreach(route => { + oauthServe(apiPrefix{route}) + }) + +} diff --git a/src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala new file mode 100644 index 000000000..9e2cd9245 --- /dev/null +++ b/src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala @@ -0,0 +1,214 @@ +package code.api.ResourceDocs + +import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0} +import net.liftweb.common.{Box, Full, Loggable} +import net.liftweb.http.rest.RestHelper +import net.liftweb.http.{JsonResponse, Req} +import net.liftweb.json.Extraction +import net.liftweb.json.JsonAST.JValue +import net.liftweb.json.JsonDSL._ +import net.liftweb.util.Props + +import scala.collection.immutable.Nil + +// JObject creation +import code.api.v1_2_1.{APIInfoJSON, APIMethods121, HostedBy, OBPAPI1_2_1} +import code.api.v1_3_0.{APIMethods130, OBPAPI1_3_0} +import code.api.v2_0_0.{APIMethods200, OBPAPI2_0_0} + +import scala.collection.mutable.ArrayBuffer + +// So we can include resource docs from future versions +import java.text.SimpleDateFormat + +import code.api.util.APIUtil.{ResourceDoc, _} +import code.model._ + +trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods140 with APIMethods130 with APIMethods121{ + //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. + // We add previous APIMethods so we have access to the Resource Docs + self: RestHelper => + + val ImplementationsResourceDocs = new Object() { + + val resourceDocs = ArrayBuffer[ResourceDoc]() + val emptyObjectJson : JValue = Nil + val apiVersion : String = "1_4_0" + + val exampleDateString : String ="22/08/2013" + val simpleDateFormat : SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") + val exampleDate = simpleDateFormat.parse(exampleDateString) + + + def getResourceDocsList(requestedApiVersion : String) : Option[List[ResourceDoc]] = + { + + // Return a different list of resource docs depending on the version being called. + // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) etc. + + val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + + // When we add a new version, update this. + val resourceDocsAll = resourceDocs2_0_0 + + val cumulativeResourceDocs = requestedApiVersion match { + case "2.0.0" => resourceDocs2_0_0 + case "1.4.0" => resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + case "1.2.1" => Implementations1_2_1.resourceDocs + case _ => resourceDocsAll + } + + // Only return APIs that are present in the list of routes for the version called + val liveResourceDocs = requestedApiVersion match { + case "2.0.0" => cumulativeResourceDocs.filter(rd => OBPAPI2_0_0.routes.contains(rd.partialFunction)) + case "1.4.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_4_0.routes.contains(rd.partialFunction)) + case "1.3.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_3_0.routes.contains(rd.partialFunction)) + case "1.2.1" => cumulativeResourceDocs.filter(rd => OBPAPI1_2_1.routes.contains(rd.partialFunction)) + case _ => cumulativeResourceDocs // Will be all ResourceDocs across all versions + } + + // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. + Some(cumulativeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) + } + + + + resourceDocs += ResourceDoc( + getResourceDocsObp(apiVersion), + apiVersion, + "getResourceDocsObp", + "GET", + "/resource-docs/obp", + "Get Resource Documentation in OBP format.", + """Returns documentation about the RESTful resources on this server including example body for POST or PUT requests. + | Thus the OBP API Explorer (and other apps) can display and work with the API documentation. + | In the future this information will be used to create Swagger (WIP) and RAML files. + |
      + |
    • operation_id is concatenation of version and function and should be unque (the aim of this is to allow links to code)
    • + |
    • version references the version that the API call is defined in.
    • + |
    • function is the (scala) function.
    • + |
    • request_url is empty for the root call, else the path.
    • + |
    • summary is a short description inline with the swagger terminology.
    • + |
    • description can contain html markup.
    • + |
    + """, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil + ) + + // Provides resource documents so that API Explorer (or other apps) can display API documentation + // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown. + def getResourceDocsObp(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "resource-docs" :: "obp" :: Nil JsonGet _ => { + user => { + for { + rd <- getResourceDocsList(requestedApiVersion) + } yield { + // Format the data as json + val json = JSONFactory1_4_0.createResourceDocsJson(rd) + // Return + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + + + resourceDocs += ResourceDoc( + getResourceDocsSwagger(apiVersion), + apiVersion, + "getResourceDocsSwagger", + "GET", + "/resource-docs/swagger", + "Get Resource Documentation in Swagger format. Work In Progress!", + """Returns documentation about the RESTful resources on this server in Swagger format. + | Currently this is incomplete. + """, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil + ) + + def getResourceDocsSwagger(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "resource-docs" :: "swagger" :: Nil JsonGet _ => { + user => { + for { + rd <- getResourceDocsList(requestedApiVersion) + } yield { + // Format the data as json + val json = SwaggerJSONFactory1_4_0.createSwaggerResourceDoc(rd) + // Return + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + + + + + + + if (Props.devMode) { + resourceDocs += ResourceDoc( + dummy(apiVersion), + apiVersion, + "testResourceDoc", + "GET", + "/dummy", + "I am only a test resource Doc", + """ + | + |#This should be H1 + | + |##This should be H2 + | + |###This should be H3 + | + |####This should be H4 + | + |Here is a list with two items: + | + |* One + |* Two + | + |There are underscores by them selves _ + | + |There are _underscores_ around a word + | + |There are underscores_in_words + | + |There are 'underscores_in_words_inside_quotes' + | + |There are (underscores_in_words_in_brackets) + | + |_etc_...""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + } + + + + def dummy(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "dummy" :: Nil JsonGet json => { + user => + val apiDetails: JValue = { + val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994") + val apiInfoJSON = new APIInfoJSON(apiVersion, gitCommit, hostedBy) + Extraction.decompose(apiInfoJSON) + } + + Full(successJsonResponse(apiDetails, 200)) + } + } + + } + + + +} + + diff --git a/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala b/src/main/scala/code/api/ResourceDocs/SwaggerJSONFactory1_4_0.scala similarity index 99% rename from src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala rename to src/main/scala/code/api/ResourceDocs/SwaggerJSONFactory1_4_0.scala index c354c5176..f1e391578 100644 --- a/src/main/scala/code/api/v1_4_0/SwaggerJSONFactory1_4_0.scala +++ b/src/main/scala/code/api/ResourceDocs/SwaggerJSONFactory1_4_0.scala @@ -1,9 +1,8 @@ -package code.api.v1_4_0 +package code.api.ResourceDocs import code.api.util.APIUtil.ResourceDoc -import net.liftweb.util.Props import net.liftweb.json._ - +import net.liftweb.util.Props import scala.collection.immutable.ListMap diff --git a/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala index 411e5033e..ad78adfc4 100644 --- a/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala +++ b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala @@ -36,10 +36,9 @@ import net.liftweb.json.JsonAST._ import net.liftweb.common.Loggable import code.api.OBPRestHelper +// Added so we can add resource docs for this version of the API -import code.api.v1_4_0.APIMethods140 // Added so we can add resource docs for this version of the API - -object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable with APIMethods140 { +object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable { val VERSION = "1.2.1" @@ -115,10 +114,7 @@ object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable with A Implementations1_2_1.updateWhereTagForViewOnTransaction, Implementations1_2_1.deleteWhereTagForViewOnTransaction, Implementations1_2_1.getCounterpartyForTransaction, - Implementations1_2_1.makePayment, - // For Resource Docs on this (1.2.1) version of the API - Implementations1_4_0.getResourceDocsObp(VERSION), - Implementations1_4_0.getResourceDocsSwagger(VERSION) + Implementations1_2_1.makePayment ) routes.foreach(route => { diff --git a/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala b/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala index 1e17f2188..380dbaca3 100644 --- a/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala +++ b/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala @@ -4,11 +4,12 @@ import code.api.OBPRestHelper import code.api.v1_2_1.APIMethods121 import net.liftweb.common.Loggable -import code.api.v1_4_0.APIMethods140 // Added so we can add resource docs for this version of the API + +// Added so we can add resource docs for this version of the API //has APIMethods121 as all api calls that went unchanged from 1.2.1 to 1.3.0 will use the old //implementation -object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 with Loggable with APIMethods140 { +object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 with Loggable { val VERSION = "1.3.0" @@ -85,10 +86,7 @@ object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 w Implementations1_2_1.getCounterpartyForTransaction, Implementations1_2_1.makePayment, Implementations1_3_0.getCards, - Implementations1_3_0.getCardsForBank, - // For Resource Docs on this (1.3.0) version of the API - Implementations1_4_0.getResourceDocsObp(VERSION), - Implementations1_4_0.getResourceDocsSwagger(VERSION) + Implementations1_3_0.getCardsForBank ) routes.foreach(route => { diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index d8add8b26..7f9ab2b7c 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -3,6 +3,7 @@ package code.api.v1_4_0 import java.text.SimpleDateFormat import java.util.Date +import code.api.v1_4_0.JSONFactory1_4_0._ import code.bankconnectors.Connector import code.transactionrequests.TransactionRequests.{TransactionRequestBody, TransactionRequestAccount} import net.liftweb.common.{Failure, Loggable, Box, Full} @@ -24,10 +25,10 @@ import collection.mutable.ArrayBuffer import code.api.APIFailure import code.api.v1_2_1.{OBPAPI1_2_1, APIInfoJSON, HostedBy, APIMethods121} import code.api.v1_3_0.{OBPAPI1_3_0, APIMethods130} -import code.api.v2_0_0.{OBPAPI2_0_0, APIMethods200} +//import code.api.v2_0_0.{OBPAPI2_0_0, APIMethods200} // So we can include resource docs from future versions -import code.api.v1_4_0.JSONFactory1_4_0._ +//import code.api.v1_4_0.JSONFactory1_4_0._ import code.atms.Atms import code.branches.Branches import code.crm.CrmEvent @@ -41,7 +42,7 @@ import java.text.SimpleDateFormat import net.liftweb.http.CurrentReq -trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with APIMethods121{ +trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. // We add previous APIMethods so we have access to the Resource Docs self: RestHelper => @@ -57,38 +58,6 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with val exampleDate = simpleDateFormat.parse(exampleDateString) - def getResourceDocsList(requestedApiVersion : String) : Option[List[ResourceDoc]] = - { - - // Return a different list of resource docs depending on the version being called. - // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) etc. - - val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs - - // When we add a new version, update this. - val resourceDocsAll = resourceDocs2_0_0 - - val cumulativeResourceDocs = requestedApiVersion match { - case "2.0.0" => resourceDocs2_0_0 - case "1.4.0" => resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs - case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs - case "1.2.1" => Implementations1_2_1.resourceDocs - case _ => resourceDocsAll - } - - // Only return APIs that are present in the list of routes for the version called - val liveResourceDocs = requestedApiVersion match { - case "2.0.0" => cumulativeResourceDocs.filter(rd => OBPAPI2_0_0.routes.contains(rd.partialFunction)) - case "1.4.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_4_0.routes.contains(rd.partialFunction)) - case "1.3.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_3_0.routes.contains(rd.partialFunction)) - case "1.2.1" => cumulativeResourceDocs.filter(rd => OBPAPI1_2_1.routes.contains(rd.partialFunction)) - case _ => cumulativeResourceDocs // Will be all ResourceDocs across all versions - } - - // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. - Some(cumulativeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) - } - resourceDocs += ResourceDoc( getCustomer, apiVersion, @@ -331,81 +300,6 @@ trait APIMethods140 extends Loggable with APIMethods200 with APIMethods130 with } } } - - resourceDocs += ResourceDoc( - getResourceDocsObp(apiVersion), - apiVersion, - "getResourceDocsObp", - "GET", - "/resource-docs/obp", - "Get Resource Documentation in OBP format.", - """Returns documentation about the RESTful resources on this server including example body for POST or PUT requests. - | Thus the OBP API Explorer (and other apps) can display and work with the API documentation. - | In the future this information will be used to create Swagger (WIP) and RAML files. - |
      - |
    • operation_id is concatenation of version and function and should be unque (the aim of this is to allow links to code)
    • - |
    • version references the version that the API call is defined in.
    • - |
    • function is the (scala) function.
    • - |
    • request_url is empty for the root call, else the path.
    • - |
    • summary is a short description inline with the swagger terminology.
    • - |
    • description can contain html markup.
    • - |
    - """, - emptyObjectJson, - emptyObjectJson, - emptyObjectJson :: Nil - ) - - // Provides resource documents so that API Explorer (or other apps) can display API documentation - // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown. - def getResourceDocsObp(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "resource-docs" :: "obp" :: Nil JsonGet _ => { - user => { - for { - rd <- getResourceDocsList(requestedApiVersion) - } yield { - // Format the data as json - val json = JSONFactory1_4_0.createResourceDocsJson(rd) - // Return - successJsonResponse(Extraction.decompose(json)) - } - } - } - } - - - resourceDocs += ResourceDoc( - getResourceDocsSwagger(apiVersion), - apiVersion, - "getResourceDocsSwagger", - "GET", - "/resource-docs/swagger", - "Get Resource Documentation in Swagger format. Work In Progress!", - """Returns documentation about the RESTful resources on this server in Swagger format. - | Currently this is incomplete. - """, - emptyObjectJson, - emptyObjectJson, - emptyObjectJson :: Nil - ) - - def getResourceDocsSwagger(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "resource-docs" :: "swagger" :: Nil JsonGet _ => { - user => { - for { - rd <- getResourceDocsList(requestedApiVersion) - } yield { - // Format the data as json - val json = SwaggerJSONFactory1_4_0.createSwaggerResourceDoc(rd) - // Return - successJsonResponse(Extraction.decompose(json)) - } - } - } - } - - - /* transaction requests (new payments since 1.4.0) */ diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index cd21657df..f1f372785 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -309,6 +309,7 @@ object JSONFactory1_4_0 { ) } + // TODO Check if this is duplicated across versions? case class AmountOfMoneyJSON ( currency : String, amount : String diff --git a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala index 3eae0ded6..916bc2ea3 100644 --- a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala @@ -93,8 +93,6 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable { Implementations1_4_0.getAtms, Implementations1_4_0.getProducts, Implementations1_4_0.getCrmEvents, - Implementations1_4_0.getResourceDocsObp(VERSION), - Implementations1_4_0.getResourceDocsSwagger(VERSION), Implementations1_4_0.createTransactionRequest, Implementations1_4_0.getTransactionRequests, Implementations1_4_0.getTransactionRequestTypes, diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala index d4b8f33db..055bff341 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -32,7 +32,7 @@ Berlin 13359, Germany package code.api.v2_0_0 import code.api.v1_3_0.APIMethods130 -import code.api.v1_4_0.APIMethods140 +import code.api.v1_4_0.{APIMethods140} import net.liftweb.json.Extraction import net.liftweb.json.JsonAST._ import net.liftweb.common.Loggable @@ -129,8 +129,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations1_4_0.getAtms, Implementations1_4_0.getProducts, Implementations1_4_0.getCrmEvents, - Implementations1_4_0.getResourceDocsObp(VERSION), - Implementations1_4_0.getResourceDocsSwagger(VERSION), + //ImplementationsResourceDocs1_4_0.getResourceDocsObp(VERSION), + //ImplementationsResourceDocs1_4_0.getResourceDocsSwagger(VERSION), Implementations1_4_0.createTransactionRequest, Implementations1_4_0.getTransactionRequests, Implementations1_4_0.getTransactionRequestTypes, From 8e409f1deaf628e8363f764d5ec191916cdf961a Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 18 Jan 2016 09:17:51 +0100 Subject: [PATCH 287/702] Tweaking path for resource-docs --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 ++-- .../ResourceDocs.scala | 9 +++++---- .../ResourceDocsAPIMethods.scala | 16 ++++++++-------- .../SwaggerJSONFactory.scala} | 4 ++-- 4 files changed, 17 insertions(+), 16 deletions(-) rename src/main/scala/code/api/{ResourceDocs => ResourceDocsRD1}/ResourceDocs.scala (58%) rename src/main/scala/code/api/{ResourceDocs => ResourceDocsRD1}/ResourceDocsAPIMethods.scala (92%) rename src/main/scala/code/api/{ResourceDocs/SwaggerJSONFactory1_4_0.scala => ResourceDocsRD1/SwaggerJSONFactory.scala} (98%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index addfb2b85..8f627d893 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -31,7 +31,7 @@ Berlin 13359, Germany */ package bootstrap.liftweb -import code.api.ResourceDocs.ResourceDocs +import code.api.ResourceDocsRD1.ResourceDocs import code.api.sandbox.SandboxApiCalls import code.crm.MappedCrmEvent import code.management.ImporterAPI @@ -67,7 +67,7 @@ import code.model._ import code.model.dataAccess._ import code.api._ import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} -import code.api.ResourceDocs.{ResourceDocsAPIMethods} +import code.api.ResourceDocsRD1.{ResourceDocsAPIMethods} diff --git a/src/main/scala/code/api/ResourceDocs/ResourceDocs.scala b/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala similarity index 58% rename from src/main/scala/code/api/ResourceDocs/ResourceDocs.scala rename to src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala index fc250481a..de0eb4955 100644 --- a/src/main/scala/code/api/ResourceDocs/ResourceDocs.scala +++ b/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala @@ -1,4 +1,4 @@ -package code.api.ResourceDocs +package code.api.ResourceDocsRD1 import code.api.OBPRestHelper @@ -8,11 +8,12 @@ import net.liftweb.common.Loggable object ResourceDocs extends OBPRestHelper with ResourceDocsAPIMethods with Loggable { - val VERSION = "1.3.0" + val VERSION = "rd1" + val routes = List( - ImplementationsResourceDocs.getResourceDocsObp(VERSION), - ImplementationsResourceDocs.getResourceDocsSwagger(VERSION) + ImplementationsResourceDocs.getResourceDocsObp, + ImplementationsResourceDocs.getResourceDocsSwagger ) routes.foreach(route => { diff --git a/src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocsRD1/ResourceDocsAPIMethods.scala similarity index 92% rename from src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala rename to src/main/scala/code/api/ResourceDocsRD1/ResourceDocsAPIMethods.scala index 9e2cd9245..eb1b5ddbd 100644 --- a/src/main/scala/code/api/ResourceDocs/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocsRD1/ResourceDocsAPIMethods.scala @@ -1,4 +1,4 @@ -package code.api.ResourceDocs +package code.api.ResourceDocsRD1 import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0} import net.liftweb.common.{Box, Full, Loggable} @@ -75,7 +75,7 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods resourceDocs += ResourceDoc( - getResourceDocsObp(apiVersion), + getResourceDocsObp, apiVersion, "getResourceDocsObp", "GET", @@ -100,8 +100,8 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods // Provides resource documents so that API Explorer (or other apps) can display API documentation // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown. - def getResourceDocsObp(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "resource-docs" :: "obp" :: Nil JsonGet _ => { + def getResourceDocsObp : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "resource-docs" :: requestedApiVersion :: "obp" :: Nil JsonGet _ => { user => { for { rd <- getResourceDocsList(requestedApiVersion) @@ -117,7 +117,7 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods resourceDocs += ResourceDoc( - getResourceDocsSwagger(apiVersion), + getResourceDocsSwagger, apiVersion, "getResourceDocsSwagger", "GET", @@ -131,14 +131,14 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods emptyObjectJson :: Nil ) - def getResourceDocsSwagger(requestedApiVersion: String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "resource-docs" :: "swagger" :: Nil JsonGet _ => { + def getResourceDocsSwagger : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "resource-docs" :: requestedApiVersion :: "swagger" :: Nil JsonGet _ => { user => { for { rd <- getResourceDocsList(requestedApiVersion) } yield { // Format the data as json - val json = SwaggerJSONFactory1_4_0.createSwaggerResourceDoc(rd) + val json = SwaggerJSONFactory.createSwaggerResourceDoc(rd) // Return successJsonResponse(Extraction.decompose(json)) } diff --git a/src/main/scala/code/api/ResourceDocs/SwaggerJSONFactory1_4_0.scala b/src/main/scala/code/api/ResourceDocsRD1/SwaggerJSONFactory.scala similarity index 98% rename from src/main/scala/code/api/ResourceDocs/SwaggerJSONFactory1_4_0.scala rename to src/main/scala/code/api/ResourceDocsRD1/SwaggerJSONFactory.scala index f1e391578..636deb6a1 100644 --- a/src/main/scala/code/api/ResourceDocs/SwaggerJSONFactory1_4_0.scala +++ b/src/main/scala/code/api/ResourceDocsRD1/SwaggerJSONFactory.scala @@ -1,4 +1,4 @@ -package code.api.ResourceDocs +package code.api.ResourceDocsRD1 import code.api.util.APIUtil.ResourceDoc import net.liftweb.json._ @@ -6,7 +6,7 @@ import net.liftweb.util.Props import scala.collection.immutable.ListMap -object SwaggerJSONFactory1_4_0 { +object SwaggerJSONFactory { case class ContactJson( name: String, From f842c05bd13f01ee37fc7bb272f06039f8e65da6 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 18 Jan 2016 16:10:54 +0100 Subject: [PATCH 288/702] Tweaking resource docs path --- src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala | 2 +- src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala b/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala index de0eb4955..3afea6b98 100644 --- a/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala +++ b/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala @@ -8,7 +8,7 @@ import net.liftweb.common.Loggable object ResourceDocs extends OBPRestHelper with ResourceDocsAPIMethods with Loggable { - val VERSION = "rd1" + val VERSION = "1.4.0" val routes = List( diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala index 055bff341..c0769e0b7 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -47,7 +47,7 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations1_2_1.root(VERSION), Implementations1_2_1.allBanks, Implementations1_2_1.bankById, - // now in v2.0.0 Implementations2_0_0.allAccountsAllBanks + // now in v2.0.0 Implementations1_2_1.allAccountsAllBanks Implementations1_2_1.privateAccountsAllBanks, Implementations1_2_1.publicAccountsAllBanks, Implementations1_2_1.allAccountsAtOneBank, @@ -129,8 +129,6 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations1_4_0.getAtms, Implementations1_4_0.getProducts, Implementations1_4_0.getCrmEvents, - //ImplementationsResourceDocs1_4_0.getResourceDocsObp(VERSION), - //ImplementationsResourceDocs1_4_0.getResourceDocsSwagger(VERSION), Implementations1_4_0.createTransactionRequest, Implementations1_4_0.getTransactionRequests, Implementations1_4_0.getTransactionRequestTypes, From 6f8219e2615858cdfcb8ea94314170b009e61f9f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 18 Jan 2016 16:45:20 +0100 Subject: [PATCH 289/702] Changing ResourceDoc package from RD1 to 1_4_0 --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 ++-- .../{ResourceDocsRD1 => ResourceDocs1_4_0}/ResourceDocs.scala | 2 +- .../ResourceDocsAPIMethods.scala | 2 +- .../SwaggerJSONFactory.scala | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/main/scala/code/api/{ResourceDocsRD1 => ResourceDocs1_4_0}/ResourceDocs.scala (91%) rename src/main/scala/code/api/{ResourceDocsRD1 => ResourceDocs1_4_0}/ResourceDocsAPIMethods.scala (99%) rename src/main/scala/code/api/{ResourceDocsRD1 => ResourceDocs1_4_0}/SwaggerJSONFactory.scala (98%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 8f627d893..93fcbac4f 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -31,7 +31,7 @@ Berlin 13359, Germany */ package bootstrap.liftweb -import code.api.ResourceDocsRD1.ResourceDocs +import code.api.ResourceDocs1_4_0.ResourceDocs import code.api.sandbox.SandboxApiCalls import code.crm.MappedCrmEvent import code.management.ImporterAPI @@ -67,7 +67,7 @@ import code.model._ import code.model.dataAccess._ import code.api._ import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} -import code.api.ResourceDocsRD1.{ResourceDocsAPIMethods} +import code.api.ResourceDocs1_4_0.{ResourceDocsAPIMethods} diff --git a/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs.scala similarity index 91% rename from src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala rename to src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs.scala index 3afea6b98..5fd207836 100644 --- a/src/main/scala/code/api/ResourceDocsRD1/ResourceDocs.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs.scala @@ -1,4 +1,4 @@ -package code.api.ResourceDocsRD1 +package code.api.ResourceDocs1_4_0 import code.api.OBPRestHelper diff --git a/src/main/scala/code/api/ResourceDocsRD1/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala similarity index 99% rename from src/main/scala/code/api/ResourceDocsRD1/ResourceDocsAPIMethods.scala rename to src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index eb1b5ddbd..dff28a23d 100644 --- a/src/main/scala/code/api/ResourceDocsRD1/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -1,4 +1,4 @@ -package code.api.ResourceDocsRD1 +package code.api.ResourceDocs1_4_0 import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0} import net.liftweb.common.{Box, Full, Loggable} diff --git a/src/main/scala/code/api/ResourceDocsRD1/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala similarity index 98% rename from src/main/scala/code/api/ResourceDocsRD1/SwaggerJSONFactory.scala rename to src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index 636deb6a1..ed6c57cc7 100644 --- a/src/main/scala/code/api/ResourceDocsRD1/SwaggerJSONFactory.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -1,4 +1,4 @@ -package code.api.ResourceDocsRD1 +package code.api.ResourceDocs1_4_0 import code.api.util.APIUtil.ResourceDoc import net.liftweb.json._ From 0caadfbc305d9495c38676ef33d4d050f597f96f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 18 Jan 2016 18:50:43 +0100 Subject: [PATCH 290/702] So we can explicitly get all Resource Docs - And strip out v and V from version number --- .../ResourceDocsAPIMethods.scala | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index dff28a23d..a9f96b07e 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -56,7 +56,11 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods case "1.4.0" => resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.2.1" => Implementations1_2_1.resourceDocs - case _ => resourceDocsAll + case "all" => resourceDocsAll + case _ => { + logger.info("requestedApiVersion not specified. Returning all available ResourceDocs") + resourceDocsAll + } } // Only return APIs that are present in the list of routes for the version called @@ -65,7 +69,11 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods case "1.4.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_4_0.routes.contains(rd.partialFunction)) case "1.3.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_3_0.routes.contains(rd.partialFunction)) case "1.2.1" => cumulativeResourceDocs.filter(rd => OBPAPI1_2_1.routes.contains(rd.partialFunction)) - case _ => cumulativeResourceDocs // Will be all ResourceDocs across all versions + case "all" => cumulativeResourceDocs + case _ => { + logger.info("requestedApiVersion not specified. Not filtering by version") + cumulativeResourceDocs + } } // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. @@ -100,11 +108,17 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods // Provides resource documents so that API Explorer (or other apps) can display API documentation // Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown. + + + // TODO constrain version? + // strip the leading v + def cleanApiVersionString (version: String) : String = {version.stripPrefix("v").stripPrefix("V")} + def getResourceDocsObp : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "resource-docs" :: requestedApiVersion :: "obp" :: Nil JsonGet _ => { user => { for { - rd <- getResourceDocsList(requestedApiVersion) + rd <- getResourceDocsList(cleanApiVersionString(requestedApiVersion)) } yield { // Format the data as json val json = JSONFactory1_4_0.createResourceDocsJson(rd) @@ -135,7 +149,7 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods case "resource-docs" :: requestedApiVersion :: "swagger" :: Nil JsonGet _ => { user => { for { - rd <- getResourceDocsList(requestedApiVersion) + rd <- getResourceDocsList(cleanApiVersionString(requestedApiVersion)) } yield { // Format the data as json val json = SwaggerJSONFactory.createSwaggerResourceDoc(rd) From 602bc60b0da81feed51d9c41c37deb1dbe2e42da Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 18 Jan 2016 21:14:00 +0100 Subject: [PATCH 291/702] Working on filtering of Resource Docs --- .../ResourceDocsAPIMethods.scala | 8 +++--- .../scala/code/api/v2_0_0/APIMethods200.scala | 27 +++++++++++++++++++ .../scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 11 +++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index a9f96b07e..e8fa5b515 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -46,14 +46,16 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods // Return a different list of resource docs depending on the version being called. // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) etc. - val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + logger.info(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion") + + val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs // When we add a new version, update this. val resourceDocsAll = resourceDocs2_0_0 val cumulativeResourceDocs = requestedApiVersion match { case "2.0.0" => resourceDocs2_0_0 - case "1.4.0" => resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs + case "1.4.0" => Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.2.1" => Implementations1_2_1.resourceDocs case "all" => resourceDocsAll @@ -77,7 +79,7 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods } // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. - Some(cumulativeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) + Some(liveResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 7b63c41e1..32eb0e6e0 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -84,6 +84,33 @@ trait APIMethods200 { } } + resourceDocs += ResourceDoc( + privateAccountsAllBanks, + apiVersion, + "privateAccountsAllBanks", + "GET", + "/accounts/private", + "Get private accounts for all banks.", + """Returns the list of private (non-public) accounts the user has access to at all banks. + |For each account the API returns the ID and the available views. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get private accounts for all banks + case "accounts" :: "private" :: Nil JsonGet json => { + user => + for { + u <- user ?~ "user not found" + } yield { + val availableAccounts = BankAccount.nonPublicAccounts(u) + successJsonResponse(bankAccountBasicListToJson(availableAccounts, Full(u))) + } + } + } } diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala index c0769e0b7..f2712ada8 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -43,12 +43,15 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w val VERSION = "2.0.0" + + // Note: Since we pattern match on these routes, if two implementations match a given url the first will match + val routes = List( Implementations1_2_1.root(VERSION), Implementations1_2_1.allBanks, Implementations1_2_1.bankById, - // now in v2.0.0 Implementations1_2_1.allAccountsAllBanks - Implementations1_2_1.privateAccountsAllBanks, +// Now implemented by 2.0.0 Implementations1_2_1.allAccountsAllBanks, +// Now implemented by 2.0.0 Implementations1_2_1.privateAccountsAllBanks, Implementations1_2_1.publicAccountsAllBanks, Implementations1_2_1.allAccountsAtOneBank, Implementations1_2_1.privateAccountsAtOneBank, @@ -136,8 +139,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w // Modified in 2.0.0 - Implementations2_0_0.allAccountsAllBanks -// Implementations1_2_1.privateAccountsAllBanks, + Implementations2_0_0.allAccountsAllBanks, + Implementations2_0_0.privateAccountsAllBanks // Implementations1_2_1.publicAccountsAllBanks, // Implementations1_2_1.allAccountsAtOneBank, // Implementations1_2_1.privateAccountsAtOneBank, From 82ec1951a533b5f20539557d1bf328904fb665fd Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 19 Jan 2016 09:59:28 +0100 Subject: [PATCH 292/702] Refactoring resource doc filtering but still WIP --- .../ResourceDocsAPIMethods.scala | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index e8fa5b515..80964f8e3 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -42,44 +42,40 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods def getResourceDocsList(requestedApiVersion : String) : Option[List[ResourceDoc]] = { - // Return a different list of resource docs depending on the version being called. // For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) etc. logger.info(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion") - val resourceDocs2_0_0 = Implementations2_0_0.resourceDocs ++ Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs - - // When we add a new version, update this. - val resourceDocsAll = resourceDocs2_0_0 - - val cumulativeResourceDocs = requestedApiVersion match { - case "2.0.0" => resourceDocs2_0_0 + val resourceDocs = requestedApiVersion match { + case "2.0.0" => Implementations2_0_0.resourceDocs ++ Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.4.0" => Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs case "1.2.1" => Implementations1_2_1.resourceDocs - case "all" => resourceDocsAll - case _ => { - logger.info("requestedApiVersion not specified. Returning all available ResourceDocs") - resourceDocsAll - } } - // Only return APIs that are present in the list of routes for the version called - val liveResourceDocs = requestedApiVersion match { - case "2.0.0" => cumulativeResourceDocs.filter(rd => OBPAPI2_0_0.routes.contains(rd.partialFunction)) - case "1.4.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_4_0.routes.contains(rd.partialFunction)) - case "1.3.0" => cumulativeResourceDocs.filter(rd => OBPAPI1_3_0.routes.contains(rd.partialFunction)) - case "1.2.1" => cumulativeResourceDocs.filter(rd => OBPAPI1_2_1.routes.contains(rd.partialFunction)) - case "all" => cumulativeResourceDocs - case _ => { - logger.info("requestedApiVersion not specified. Not filtering by version") - cumulativeResourceDocs - } + logger.info(s"There are ${resourceDocs.length} resource docs available to $requestedApiVersion") + + val versionRoutes = requestedApiVersion match { + case "2.0.0" => OBPAPI2_0_0.routes + case "1.4.0" => OBPAPI1_4_0.routes + case "1.3.0" => OBPAPI1_3_0.routes + case "1.2.1" => OBPAPI1_2_1.routes } + logger.info(s"There are ${versionRoutes.length} routes available to $requestedApiVersion") + + // Filter out the resource docs whose partialFunction is not contained in the list of routes + // i.e. we only want the resource docs for which a API route exists else users will see 404s + // filter/contains not working. + //val activeResourceDocs = resourceDocs.filter(rd => versionRoutes.contains(rd.partialFunction)) + + val activeResourceDocs = resourceDocs + + logger.info(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion") + // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that. - Some(liveResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) + Some(activeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb))) } From ca6f642a9fe8be211bd2d309bc5384e5adbbc8dd Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 19 Jan 2016 14:42:06 +0100 Subject: [PATCH 293/702] ResourceDocs filtering --- .../code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 80964f8e3..0801879ff 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -68,9 +68,8 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods // Filter out the resource docs whose partialFunction is not contained in the list of routes // i.e. we only want the resource docs for which a API route exists else users will see 404s // filter/contains not working. - //val activeResourceDocs = resourceDocs.filter(rd => versionRoutes.contains(rd.partialFunction)) - - val activeResourceDocs = resourceDocs + val versionRoutesClasses = versionRoutes.map { vr => vr.getClass } + val activeResourceDocs = resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass)) logger.info(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion") From d54c06e3c2ceef2a7c8d67901e881d030d0f57b7 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 19 Jan 2016 15:04:14 +0100 Subject: [PATCH 294/702] Tweaking documentation of resource doc filtering --- .../api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 0801879ff..96e65e2b0 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -65,10 +65,12 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods logger.info(s"There are ${versionRoutes.length} routes available to $requestedApiVersion") - // Filter out the resource docs whose partialFunction is not contained in the list of routes - // i.e. we only want the resource docs for which a API route exists else users will see 404s - // filter/contains not working. + + // We only want the resource docs for which a API route exists else users will see 404s + // Get a list of the partial function classes represented in the routes available to this version. val versionRoutesClasses = versionRoutes.map { vr => vr.getClass } + + // Only return the resource docs that have available routes val activeResourceDocs = resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass)) logger.info(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion") From b4d204b4194e4e513c9bded7404e2cd6c3fcce20 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 19 Jan 2016 16:38:29 +0100 Subject: [PATCH 295/702] Added Kafka instructions to README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 7457a0119..155c86af3 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,11 @@ The current workaround is to move the project directory onto a different partiti The default database for testing etc is H2. PostgreSQL is used for the sandboxes (user accounts, metadata, transaction cache). + +# Kafka (optional): + +If Kafka connector is selected in props (connector=kafka), Kafka and Zookeeper have to be installed, as well as OBP-Kafka-Python (which can be either running from command-propmpt or from inside Docker container): + + Kafka and Zookeeper can be installed using system's default installer or by unpacking the archives (http://apache.mirrors.spacedump.net/kafka/ and http://apache.mirrors.spacedump.net/zookeeper/) + OBP-Kafka-Python can be downloaded from https://github.com/OpenBankProject/OBP-Kafka-Python + From cc4c9f947bb0479b05c47966733b12f4fa232569 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 19 Jan 2016 16:40:53 +0100 Subject: [PATCH 296/702] Cleanup of README file --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 155c86af3..77d4445f8 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ The default database for testing etc is H2. PostgreSQL is used for the sandboxes If Kafka connector is selected in props (connector=kafka), Kafka and Zookeeper have to be installed, as well as OBP-Kafka-Python (which can be either running from command-propmpt or from inside Docker container): - Kafka and Zookeeper can be installed using system's default installer or by unpacking the archives (http://apache.mirrors.spacedump.net/kafka/ and http://apache.mirrors.spacedump.net/zookeeper/) - OBP-Kafka-Python can be downloaded from https://github.com/OpenBankProject/OBP-Kafka-Python +* Kafka and Zookeeper can be installed using system's default installer or by unpacking the archives (http://apache.mirrors.spacedump.net/kafka/ and http://apache.mirrors.spacedump.net/zookeeper/) + +* OBP-Kafka-Python can be downloaded from https://github.com/OpenBankProject/OBP-Kafka-Python From 4d1b7ede2f82fed54ad35c9da4136a610271fb6d Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 19 Jan 2016 23:01:06 +0100 Subject: [PATCH 297/702] Adding some 2_0_0 calls. Modifying resource doc text --- .../scala/code/api/v1_2_1/APIMethods121.scala | 2 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 122 +++++++++++++++++- .../scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 27 ++-- src/main/scala/code/views/MapperViews.scala | 2 + 4 files changed, 135 insertions(+), 18 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index a72c77b73..155b1a582 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -110,7 +110,7 @@ trait APIMethods121 { "allBanks", "GET", "/banks", - "Returns all banks available on this API instance", + "Get banks on this API instance", """Returns a list of banks supported on this server: | |* ID used as parameter in URLs diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 32eb0e6e0..5f29b0277 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -64,7 +64,7 @@ trait APIMethods200 { "allAccountsAllBanks", "GET", "/accounts", - "Get all accounts a user has access to at all banks (private + public)", + "Get accounts a user can view (all banks).", """Returns the list of accounts at that the user has access to at all banks. |For each account the API returns the account ID and the available views. | @@ -90,8 +90,8 @@ trait APIMethods200 { "privateAccountsAllBanks", "GET", "/accounts/private", - "Get private accounts for all banks.", - """Returns the list of private (non-public) accounts the user has access to at all banks. + "Get accounts a user has privileged access to (all banks).", + """Returns the list of accounts containing private (non-public) views for the user at all banks. |For each account the API returns the ID and the available views. | |Authentication via OAuth is required.""", @@ -113,6 +113,122 @@ trait APIMethods200 { } + resourceDocs += ResourceDoc( + publicAccountsAllBanks, + apiVersion, + "publicAccountsAllBanks", + "GET", + "/accounts/public", + "Get accounts that have public views (all banks).", + """Returns the list of accounts containing public views at all banks + |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val publicAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get public accounts for all banks + case "accounts" :: "public" :: Nil JsonGet json => { + user => + val publicAccountsJson = bankAccountBasicListToJson(BankAccount.publicAccounts, Empty) + Full(successJsonResponse(publicAccountsJson)) + } + } + + + + + resourceDocs += ResourceDoc( + allAccountsAtOneBank, + apiVersion, + "allAccountsAtOneBank", + "GET", + "/banks/BANK_ID/accounts", + "Get accounts for a single bank the user has either private or public access to.", + """Returns the list of accounts at BANK_ID that the user has access to. + |For each account the API returns the account ID and the available views. + | + |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views. + """, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val allAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get accounts for a single bank (private + public) + case "banks" :: BankId(bankId) :: "accounts" :: Nil JsonGet json => { + user => + for{ + bank <- Bank(bankId) + } yield { + val availableAccounts = bank.accounts(user) + successJsonResponse(bankAccountBasicListToJson(availableAccounts, user)) + } + } + } + + resourceDocs += ResourceDoc( + privateAccountsAtOneBank, + apiVersion, + "privateAccountsAtOneBank", + "GET", + "/banks/BANK_ID/accounts/private", + "Get accounts at one bank where the user has private access.", + """Returns the list of private (non-public) accounts at BANK_ID that the user has access to. + |For each account the API returns the ID and the available views. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val privateAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get private accounts for a single bank + case "banks" :: BankId(bankId) :: "accounts" :: "private" :: Nil JsonGet json => { + user => + for { + u <- user ?~ "user not found" + bank <- Bank(bankId) + } yield { + val availableAccounts = bank.nonPublicAccounts(u) + successJsonResponse(bankAccountBasicListToJson(availableAccounts, Full(u))) + } + } + } + + resourceDocs += ResourceDoc( + publicAccountsAtOneBank, + apiVersion, + "publicAccountsAtOneBank", + "GET", + "/banks/BANK_ID/accounts/public", + "Get accounts (public) for a single bank.", + """Returns a list of the public accounts at BANK_ID. For each account the API returns the ID and the available views. + | + |Authentication via OAuth is not required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val publicAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get public accounts for a single bank + case "banks" :: BankId(bankId) :: "accounts" :: "public" :: Nil JsonGet json => { + user => + for { + bank <- Bank(bankId) + } yield { + val publicAccountsJson = bankAccountBasicListToJson(bank.publicAccounts, Empty) + successJsonResponse(publicAccountsJson) + } + } + } + + + + + + + } } diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala index f2712ada8..a32ee54c8 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -50,12 +50,13 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations1_2_1.root(VERSION), Implementations1_2_1.allBanks, Implementations1_2_1.bankById, -// Now implemented by 2.0.0 Implementations1_2_1.allAccountsAllBanks, -// Now implemented by 2.0.0 Implementations1_2_1.privateAccountsAllBanks, - Implementations1_2_1.publicAccountsAllBanks, - Implementations1_2_1.allAccountsAtOneBank, - Implementations1_2_1.privateAccountsAtOneBank, - Implementations1_2_1.publicAccountsAtOneBank, + // Now in 2_0_0 +// Implementations1_2_1.allAccountsAllBanks, +// Implementations1_2_1.privateAccountsAllBanks, +// Implementations1_2_1.publicAccountsAllBanks, +// Implementations1_2_1.allAccountsAtOneBank, +// Implementations1_2_1.privateAccountsAtOneBank, +// Implementations1_2_1.publicAccountsAtOneBank, Implementations1_2_1.accountById, Implementations1_2_1.updateAccountLabel, Implementations1_2_1.getViewsForBankAccount, @@ -137,15 +138,13 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations1_4_0.getTransactionRequestTypes, Implementations1_4_0.answerTransactionRequestChallenge, - // Modified in 2.0.0 - + // Modified in 2.0.0 (less info about the views) Implementations2_0_0.allAccountsAllBanks, - Implementations2_0_0.privateAccountsAllBanks -// Implementations1_2_1.publicAccountsAllBanks, -// Implementations1_2_1.allAccountsAtOneBank, -// Implementations1_2_1.privateAccountsAtOneBank, -// Implementations1_2_1.publicAccountsAtOneBank, -// Implementations1_2_1.accountById + Implementations2_0_0.privateAccountsAllBanks, + Implementations2_0_0.publicAccountsAllBanks, + Implementations2_0_0.allAccountsAtOneBank, + Implementations2_0_0.privateAccountsAtOneBank, + Implementations2_0_0.publicAccountsAtOneBank ) routes.foreach(route => { diff --git a/src/main/scala/code/views/MapperViews.scala b/src/main/scala/code/views/MapperViews.scala index 73f94fffa..f87d14ed7 100644 --- a/src/main/scala/code/views/MapperViews.scala +++ b/src/main/scala/code/views/MapperViews.scala @@ -243,6 +243,8 @@ private object MapperViews extends Views with Loggable { def getAllPublicAccounts() : List[BankAccount] = { //TODO: do this more efficiently + // An account is considered public if it contains a public view + val bankAndAccountIds : List[(BankId, AccountId)] = ViewImpl.findAll(By(ViewImpl.isPublic_, true)).map(v => (v.bankId, v.accountId) From 8d341b7af9969d0c7cdf68c90299149ce5a39ddc Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 20 Jan 2016 00:10:36 +0100 Subject: [PATCH 298/702] Tweaking Resource Docs API documentation --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 5f29b0277..084daa076 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -64,7 +64,7 @@ trait APIMethods200 { "allAccountsAllBanks", "GET", "/accounts", - "Get accounts a user can view (all banks).", + "Get accounts for a user. Includes public accounts. All banks.", """Returns the list of accounts at that the user has access to at all banks. |For each account the API returns the account ID and the available views. | @@ -90,8 +90,8 @@ trait APIMethods200 { "privateAccountsAllBanks", "GET", "/accounts/private", - "Get accounts a user has privileged access to (all banks).", - """Returns the list of accounts containing private (non-public) views for the user at all banks. + "Get accounts for a user. All banks.", + """Returns the list of accounts containing private views for the user at all banks. |For each account the API returns the ID and the available views. | |Authentication via OAuth is required.""", @@ -144,7 +144,7 @@ trait APIMethods200 { "allAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts", - "Get accounts for a single bank the user has either private or public access to.", + "Get accounts for a user. Includes public accounts at one bank.", """Returns the list of accounts at BANK_ID that the user has access to. |For each account the API returns the account ID and the available views. | @@ -173,8 +173,8 @@ trait APIMethods200 { "privateAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/private", - "Get accounts at one bank where the user has private access.", - """Returns the list of private (non-public) accounts at BANK_ID that the user has access to. + "Get accounts that grant access to a user (one bank).", + """Returns the list of accounts containing private views for the user at BANK_ID. |For each account the API returns the ID and the available views. | |Authentication via OAuth is required.""", @@ -202,7 +202,7 @@ trait APIMethods200 { "publicAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/public", - "Get accounts (public) for a single bank.", + "Get accounts that have public views for a single bank.", """Returns a list of the public accounts at BANK_ID. For each account the API returns the ID and the available views. | |Authentication via OAuth is not required.""", From 190339ca908d2bc9b2f79a90418d596913c52dc1 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 20 Jan 2016 10:11:14 +0100 Subject: [PATCH 299/702] Updating resource docs content --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 12 ++++++------ src/main/scala/code/api/v2_0_0/APIMethods200.scala | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 155b1a582..cc9f72bf9 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -175,7 +175,7 @@ trait APIMethods121 { "allAccountsAllBanks", "GET", "/accounts", - "Get all accounts a user has access to at all banks (private + public)", + "Get accounts at all banks (Authenticated + Anonymous access).", """Returns the list of accounts at that the user has access to at all banks. |For each account the API returns the account ID and the available views. | @@ -205,7 +205,7 @@ trait APIMethods121 { "privateAccountsAllBanks", "GET", "/accounts/private", - "Get private accounts for all banks.", + "Get private accounts at all banks (Authenticated access).", """Returns the list of private (non-public) accounts the user has access to at all banks. |For each account the API returns the ID and the available views. | @@ -233,7 +233,7 @@ trait APIMethods121 { "publicAccountsAllBanks", "GET", "/accounts/public", - "Get public accounts for all banks.", + "Get public accounts at all banks (Anonymous access).", """Returns the list of private (non-public) accounts the user has access to at all banks. |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", emptyObjectJson, @@ -255,7 +255,7 @@ trait APIMethods121 { "allAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts", - "Get accounts for a single bank (private + public).", + "Get accounts at one bank (Autheneticated + Anonymous access).", """Returns the list of accounts at BANK_ID that the user has access to. |For each account the API returns the account ID and the available views. | @@ -288,7 +288,7 @@ trait APIMethods121 { "privateAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/private", - "Get accounts (private) for a single bank.", + "Get private accounts at one bank (Authenticated access).", """Returns the list of private (non-public) accounts at BANK_ID that the user has access to. |For each account the API returns the ID and the available views. | @@ -317,7 +317,7 @@ trait APIMethods121 { "publicAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/public", - "Get accounts (public) for a single bank.", + "Get public accounts at one bank (Anonymous access).", """Returns a list of the public accounts at BANK_ID. For each account the API returns the ID and the available views. | |Authentication via OAuth is not required.""", diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 084daa076..e8800052f 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -64,7 +64,7 @@ trait APIMethods200 { "allAccountsAllBanks", "GET", "/accounts", - "Get accounts for a user. Includes public accounts. All banks.", + "Get accounts at all banks (Authenticated + Anonymous access).", """Returns the list of accounts at that the user has access to at all banks. |For each account the API returns the account ID and the available views. | @@ -90,7 +90,7 @@ trait APIMethods200 { "privateAccountsAllBanks", "GET", "/accounts/private", - "Get accounts for a user. All banks.", + "Get private accounts at all banks (Authenticated access).", """Returns the list of accounts containing private views for the user at all banks. |For each account the API returns the ID and the available views. | @@ -119,7 +119,7 @@ trait APIMethods200 { "publicAccountsAllBanks", "GET", "/accounts/public", - "Get accounts that have public views (all banks).", + "Get public accounts at all banks (Anonymous access).", """Returns the list of accounts containing public views at all banks |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", emptyObjectJson, @@ -144,7 +144,7 @@ trait APIMethods200 { "allAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts", - "Get accounts for a user. Includes public accounts at one bank.", + "Get accounts at one bank (Authenticated + Anonymous access).", """Returns the list of accounts at BANK_ID that the user has access to. |For each account the API returns the account ID and the available views. | @@ -173,7 +173,7 @@ trait APIMethods200 { "privateAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/private", - "Get accounts that grant access to a user (one bank).", + "Get private accounts at one bank (Authenticated access).", """Returns the list of accounts containing private views for the user at BANK_ID. |For each account the API returns the ID and the available views. | @@ -202,7 +202,7 @@ trait APIMethods200 { "publicAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/public", - "Get accounts that have public views for a single bank.", + "Get public accounts at one bank (Anonymous access).", """Returns a list of the public accounts at BANK_ID. For each account the API returns the ID and the available views. | |Authentication via OAuth is not required.""", From 55ef0d8b2feca0e65bff7994c9ba5771cfb5e389 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 20 Jan 2016 12:09:49 +0100 Subject: [PATCH 300/702] Adding new Props settings e.g. apiOptions.getBranchesIsPublic --- src/main/resources/props/sample.props.template | 8 +++++++- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index d91ffaf07..986542c41 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -135,4 +135,10 @@ webui_main_partners=[\ webui_main_style_sheet = /media/css/website.css # Override certain elements (with important styles) -webui_override_style_sheet = \ No newline at end of file +webui_override_style_sheet = + + +## API Options +apiOptions.getBranchesIsPublic = true +apiOptions.getAtmsIsPublic = true +apiOptions.getProductsIsPublic = true \ No newline at end of file diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 7f9ab2b7c..8a2037d05 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -171,6 +171,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ emptyObjectJson :: Nil ) + val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true) + lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { From 5267fc14f3ff38f05c9be39b49387ea427704235 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 20 Jan 2016 14:20:53 +0100 Subject: [PATCH 301/702] Do not check if user is logged in when apiOptions.getBranchesIsPublic in props is true --- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 8a2037d05..566b66728 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -177,7 +177,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { for { - u <- user ?~! "User must be logged in to retrieve Branches data" + u <- if(getBranchesIsPublic) + user + else + user ?~! "User must be logged in to retrieve Branches data" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} // Get branches from the active provider branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branches available. License may not be set.", 204) From 01bee70eb918b4ef954401a8ef49d2347a310a8e Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 20 Jan 2016 15:37:53 +0100 Subject: [PATCH 302/702] authentcationRequiredMessage and making Auth optional on Atms, Products WIP --- src/main/scala/code/api/util/APIUtil.scala | 13 +++++++ .../scala/code/api/v1_4_0/APIMethods140.scala | 37 +++++++++++++------ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index af9b4347c..0e8ed27c6 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -278,4 +278,17 @@ object APIUtil extends Loggable { exampleRequestBody: JValue, // An example of the body required (maybe empty) successResponseBody: JValue, // A successful response body errorResponseBodies: List[JValue]) // Possible error responses + + + + + + def authenticationRequiredMessage(authRequired: Boolean) : String = + authRequired match { + case true => "Authentication IS required" + case false => "Authentication is NOT required" + } + } + + diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 566b66728..2462909a1 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -42,6 +42,9 @@ import java.text.SimpleDateFormat import net.liftweb.http.CurrentReq + +import code.api.util.APIUtil.authenticationRequiredMessage + trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. // We add previous APIMethods so we have access to the Resource Docs @@ -151,6 +154,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } } + val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true) + resourceDocs += ResourceDoc( getBranches, apiVersion, @@ -158,26 +163,26 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/banks/BANK_ID/branches", "Get branches for the bank", - """Returns information about branches for a single bank specified by BANK_ID including: + s"""Returns information about branches for a single bank specified by BANK_ID including: | |* Name |* Address |* Geo Location |* License the data under this endpoint is released under | - |Authentication via OAuth *may* be required.""", + |${authenticationRequiredMessage(!getBranchesIsPublic)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil ) - val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true) + lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { for { - u <- if(getBranchesIsPublic) + u <- if(getBranchesIsPublic) // If the call is public, don't worry about the user. user else user ?~! "User must be logged in to retrieve Branches data" @@ -193,7 +198,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } } - + val getAtmsIsPublic = Props.getBool("apiOptions.getAtmsIsPublic", true) resourceDocs += ResourceDoc( getAtms, @@ -202,13 +207,13 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/banks/BANK_ID/atms", "Get ATMS for the bank", - """Returns information about ATMs for a single bank specified by BANK_ID including: + s"""Returns information about ATMs for a single bank specified by BANK_ID including: | |* Address |* Geo Location |* License the data under this endpoint is released under | - |Authentication via OAuth *may* be required.""", + |${authenticationRequiredMessage(!getAtmsIsPublic)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil @@ -219,7 +224,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { // Get atms from the active provider - u <- user ?~! "User must be logged in to retrieve ATM data" + u <- if(getAtmsIsPublic) // If the call is public, don't worry about the user. + user + else + user ?~! "User must be logged in to retrieve ATM data" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} atms <- Box(Atms.atmsProvider.vend.getAtms(bankId)) ~> APIFailure("No ATMs available. License may not be set.", 204) } yield { @@ -233,6 +241,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } + val getProductsIsPublic = Props.getBool("apiOptions.getProductsIsPublic", true) + resourceDocs += ResourceDoc( getProducts, apiVersion, @@ -240,7 +250,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/banks/BANK_ID/products", "Get products offered by the bank", - """Returns information about financial products offered by a bank specified by BANK_ID including: + s"""Returns information about financial products offered by a bank specified by BANK_ID including: | |* Name |* Code @@ -250,7 +260,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |* More info URL |* Description |* Terms and Conditions - |* License the data under this endpoint is released under""", + |* License the data under this endpoint is released under + |${authenticationRequiredMessage(!getProductsIsPublic)} + |""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil @@ -261,7 +273,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { // Get products from the active provider - u <- user ?~! "User must be logged in to retrieve Products data" + u <- if(getProductsIsPublic) // If the call is public, don't worry about the user. + user + else + user ?~! "User must be logged in to retrieve Products data" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} products <- Box(Products.productsProvider.vend.getProducts(bankId)) ~> APIFailure("No products available. License may not be set.", 204) } yield { From 7baeac3395e18161d3de2992ad5d3f49f38b0dc7 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 20 Jan 2016 16:27:03 +0100 Subject: [PATCH 303/702] Do not check if user is logged in when apiOptions.getBranchesIsPublic in props is true. fix --- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 566b66728..9096badb1 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -178,7 +178,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { u <- if(getBranchesIsPublic) - user + Box(Some(1)) else user ?~! "User must be logged in to retrieve Branches data" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} From e7aa634a0cacb564e244de224728ced7811ac5da Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 20 Jan 2016 22:29:43 +0100 Subject: [PATCH 304/702] Adding authenticationRequiredMessage for getBranches, getAtms, getProducts (was in 01bee70eb) --- src/main/scala/code/api/util/APIUtil.scala | 12 +++++++ .../scala/code/api/v1_4_0/APIMethods140.scala | 36 +++++++++++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index af9b4347c..ba1509b97 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -278,4 +278,16 @@ object APIUtil extends Loggable { exampleRequestBody: JValue, // An example of the body required (maybe empty) successResponseBody: JValue, // A successful response body errorResponseBodies: List[JValue]) // Possible error responses + + def authenticationRequiredMessage(authRequired: Boolean) : String = + authRequired match { + case true => "Authentication IS required" + case false => "Authentication is NOT required" + } + + + + + + } diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 9096badb1..b22749ef6 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -40,7 +40,8 @@ import code.util.Helper._ import code.api.util.APIUtil.ResourceDoc import java.text.SimpleDateFormat -import net.liftweb.http.CurrentReq +import code.api.util.APIUtil.authenticationRequiredMessage + trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ //needs to be a RestHelper to get access to JsonGet, JsonPost, etc. @@ -151,6 +152,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } } + + val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true) + resourceDocs += ResourceDoc( getBranches, apiVersion, @@ -158,21 +162,19 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/banks/BANK_ID/branches", "Get branches for the bank", - """Returns information about branches for a single bank specified by BANK_ID including: + s"""Returns information about branches for a single bank specified by BANK_ID including: | |* Name |* Address |* Geo Location |* License the data under this endpoint is released under | - |Authentication via OAuth *may* be required.""", + |${authenticationRequiredMessage(!getBranchesIsPublic)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil ) - val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true) - lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => { user => { @@ -194,6 +196,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } + val getAtmsIsPublic = Props.getBool("apiOptions.getAtmsIsPublic", true) resourceDocs += ResourceDoc( getAtms, @@ -202,13 +205,13 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/banks/BANK_ID/atms", "Get ATMS for the bank", - """Returns information about ATMs for a single bank specified by BANK_ID including: + s"""Returns information about ATMs for a single bank specified by BANK_ID including: | |* Address |* Geo Location |* License the data under this endpoint is released under | - |Authentication via OAuth *may* be required.""", + |${authenticationRequiredMessage(!getAtmsIsPublic)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil @@ -219,7 +222,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { // Get atms from the active provider - u <- user ?~! "User must be logged in to retrieve ATM data" + + u <- if(getAtmsIsPublic) + Box(Some(1)) + else + user ?~! "User must be logged in to retrieve ATM data" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} atms <- Box(Atms.atmsProvider.vend.getAtms(bankId)) ~> APIFailure("No ATMs available. License may not be set.", 204) } yield { @@ -233,6 +240,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ } + val getProductsIsPublic = Props.getBool("apiOptions.getProductsIsPublic", true) + + resourceDocs += ResourceDoc( getProducts, apiVersion, @@ -240,7 +250,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "GET", "/banks/BANK_ID/products", "Get products offered by the bank", - """Returns information about financial products offered by a bank specified by BANK_ID including: + s"""Returns information about financial products offered by a bank specified by BANK_ID including: | |* Name |* Code @@ -250,7 +260,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |* More info URL |* Description |* Terms and Conditions - |* License the data under this endpoint is released under""", + |* License the data under this endpoint is released under + |${authenticationRequiredMessage(!getProductsIsPublic)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil @@ -261,7 +272,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { // Get products from the active provider - u <- user ?~! "User must be logged in to retrieve Products data" + u <- if(getProductsIsPublic) + Box(Some(1)) + else + user ?~! "User must be logged in to retrieve Products data" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} products <- Box(Products.productsProvider.vend.getProducts(bankId)) ~> APIFailure("No products available. License may not be set.", 204) } yield { From de37e659601cc1eb44e72425770e6ca6b6341fea Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 22 Jan 2016 18:11:39 +0100 Subject: [PATCH 305/702] Added JWT (from io.igl) as dependency --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 56d64c7eb..d54af82f7 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,11 @@ pegdown 1.6.0 + + io.igl + jwt_2.11 + 1.2.0 + From 5a02ecb5402fa90bffe8b8437ed145dde38eea10 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 23 Jan 2016 08:52:21 +0300 Subject: [PATCH 306/702] Company name -> TESOBE --- Harmony_Individual_Contributor_Assignment_Agreement.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Harmony_Individual_Contributor_Assignment_Agreement.txt b/Harmony_Individual_Contributor_Assignment_Agreement.txt index 26226395f..d3eb7ae9c 100644 --- a/Harmony_Individual_Contributor_Assignment_Agreement.txt +++ b/Harmony_Individual_Contributor_Assignment_Agreement.txt @@ -91,9 +91,9 @@ Address: ________________________ Us ________________________ Name: Simon Redfern -Title: CEO TESOBE / Music Pictures Ltd +Title: CEO TESOBE Ltd Address: Osloerstrasse 16/17 - Berlin 13359, German + Berlin 13359, Germany @@ -125,4 +125,4 @@ Address: Osloerstrasse 16/17 -This work is licensed under a Creative Commons Attribution 3.0 Unported License. \ No newline at end of file +This work is licensed under a Creative Commons Attribution 3.0 Unported License. From 68945dbc18ce24fa66e4312b6adc3fa882de69b6 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 26 Jan 2016 16:46:32 +0000 Subject: [PATCH 307/702] Trying an approach to single bank endpoints --- .../scala/code/api/v2_0_0/APIMethods200.scala | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index e8800052f..d127409ad 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -182,6 +182,14 @@ trait APIMethods200 { emptyObjectJson, emptyObjectJson :: Nil) + + def privateAccountsAtOneBankResult (bank: Bank, u: User) = { + val availableAccounts = bank.nonPublicAccounts(u) + successJsonResponse(bankAccountBasicListToJson(availableAccounts, Full(u))) + } + + // This contains an approach to surface the same resource via different end point in case of "one" bank. + // Experimental and might be removed. lazy val privateAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for a single bank case "banks" :: BankId(bankId) :: "accounts" :: "private" :: Nil JsonGet json => { @@ -190,10 +198,19 @@ trait APIMethods200 { u <- user ?~ "user not found" bank <- Bank(bankId) } yield { - val availableAccounts = bank.nonPublicAccounts(u) - successJsonResponse(bankAccountBasicListToJson(availableAccounts, Full(u))) + privateAccountsAtOneBankResult(bank, u) } } + case "one" :: "accounts" :: "private" :: Nil JsonGet json => { + user => + for { + u <- user ?~ "user not found" + bank <- Bank(BankId("abc")) + } yield { + privateAccountsAtOneBankResult(bank, u) + } + } + } resourceDocs += ResourceDoc( From 9f106413e668c61c441274b4996715efc832c440 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 27 Jan 2016 14:40:51 +0100 Subject: [PATCH 308/702] Added override styles for ulster.css --- src/main/webapp/media/css/overrides/ulster.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/media/css/overrides/ulster.css b/src/main/webapp/media/css/overrides/ulster.css index e69de29bb..720c28524 100644 --- a/src/main/webapp/media/css/overrides/ulster.css +++ b/src/main/webapp/media/css/overrides/ulster.css @@ -0,0 +1,4 @@ +.about-text li +{ + color: white; +} From e311229903fbbd174fec008198d0cfa97e7942a3 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 27 Jan 2016 14:47:56 +0100 Subject: [PATCH 309/702] Further prettification of ulster.css --- src/main/webapp/media/css/overrides/ulster.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/webapp/media/css/overrides/ulster.css b/src/main/webapp/media/css/overrides/ulster.css index 720c28524..db0fb1411 100644 --- a/src/main/webapp/media/css/overrides/ulster.css +++ b/src/main/webapp/media/css/overrides/ulster.css @@ -1,4 +1,11 @@ +.about-text ul +{ + list-style-type: initial; +} + .about-text li { color: white; + text-align: left; + margin-left: 10%; } From a6de9748744317b65bd87c03950a6dbfd4e2f30c Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 27 Jan 2016 15:26:05 +0100 Subject: [PATCH 310/702] ulster.css even prettier --- .../webapp/media/css/overrides/ulster.css | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/media/css/overrides/ulster.css b/src/main/webapp/media/css/overrides/ulster.css index db0fb1411..56f0a96f5 100644 --- a/src/main/webapp/media/css/overrides/ulster.css +++ b/src/main/webapp/media/css/overrides/ulster.css @@ -1,11 +1,29 @@ -.about-text ul +#main-about h1 { + color: white; + font-size: 35px; + margin-bottom: 15px; + margin-left: -5px; +} + +#main-about p { + color: white; + font-weight: bold; + margin-top: 10px; + margin-bottom: 10px; +} + +#main-about ul { list-style-type: initial; } -.about-text li +#main-about li { color: white; text-align: left; - margin-left: 10%; + margin-left: 100px; +} + +#main-about img { + margin-top: 30px; } From 49343c77dbaea507e39a147964a12003178ae811 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 28 Jan 2016 10:14:43 +0100 Subject: [PATCH 311/702] Fixed JWT dependency --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index d54af82f7..0f252008f 100644 --- a/pom.xml +++ b/pom.xml @@ -162,9 +162,9 @@ 1.6.0 - io.igl - jwt_2.11 - 1.2.0 + com.jason-goodwin + authentikat-jwt_2.10 + 0.4.1 From d7cb300dea336dc83455f770afec00b24d1b29c4 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 28 Jan 2016 10:19:13 +0100 Subject: [PATCH 312/702] Removed JWT dependency (from io.igl) --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0f252008f..56d64c7eb 100644 --- a/pom.xml +++ b/pom.xml @@ -161,11 +161,6 @@ pegdown 1.6.0 - - com.jason-goodwin - authentikat-jwt_2.10 - 0.4.1 - From 30aa865931d8172be9907ede8cf47458519d6670 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 28 Jan 2016 10:19:53 +0100 Subject: [PATCH 313/702] Added JWT dependency --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 56d64c7eb..7d259c4d1 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,11 @@ pegdown 1.6.0 + + com.jason-goodwin + authentikat-jwt_2.10 + 0.4.1 + From bd376d33e56ff937f3409863984cdc415d2379eb Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 28 Jan 2016 13:27:58 +0100 Subject: [PATCH 314/702] Add new fields to the Customer --- .../scala/code/api/v1_4_0/APIMethods140.scala | 14 ++++++- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 20 +++++++++- .../code/customer/CustomerProvider.scala | 18 ++++++++- .../customer/MappedCustomerProvider.scala | 32 +++++++++++++++- .../scala/code/api/v1_4_0/CustomerTest.scala | 37 +++++++++++++++---- .../v1_4_0/MappedCustomerMessagesTest.scala | 10 ++++- 6 files changed, 116 insertions(+), 15 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index b22749ef6..fe6e0b4a1 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -5,6 +5,7 @@ import java.util.Date import code.api.v1_4_0.JSONFactory1_4_0._ import code.bankconnectors.Connector +import code.metadata.comments.MappedComment import code.transactionrequests.TransactionRequests.{TransactionRequestBody, TransactionRequestAccount} import net.liftweb.common.{Failure, Loggable, Box, Full} import net.liftweb.http.js.JE.JsRaw @@ -498,7 +499,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |OAuth authentication is required. |""", Extraction.decompose(CustomerJson("687687678", "Joe David Bloggs", - "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate))), + "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), + exampleDate, "Single", 1, Array(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil) @@ -517,7 +519,15 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ postedData.legal_name, postedData.mobile_phone_number, postedData.email, - MockCustomerFaceImage(postedData.face_image.date, postedData.face_image.url)) ?~! "Could not create customer" + MockCustomerFaceImage(postedData.face_image.date, postedData.face_image.url), + postedData.date_of_birth, + postedData.relationship_status, + postedData.dependants, + postedData.dob_of_dependants, + postedData.highest_education_attained, + postedData.employment_status, + postedData.kyc_status, + postedData.last_ok_date) ?~! "Could not create customer" } yield { val successJson = Extraction.decompose(customer) successJsonResponse(successJson) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index f1f372785..bb0f33325 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -23,7 +23,15 @@ object JSONFactory1_4_0 { legal_name : String, mobile_phone_number : String, email : String, - face_image : CustomerFaceImageJson) + face_image : CustomerFaceImageJson, + date_of_birth: Date, + relationship_status: String, + dependants: Int, + dob_of_dependants: Array[Date], + highest_education_attained: String, + employment_status: String, + kyc_status: Boolean, + last_ok_date: Date) case class CustomerFaceImageJson(url : String, date : Date) @@ -77,7 +85,15 @@ object JSONFactory1_4_0 { mobile_phone_number = cInfo.mobileNumber, email = cInfo.email, face_image = CustomerFaceImageJson(url = cInfo.faceImage.url, - date = cInfo.faceImage.date) + date = cInfo.faceImage.date), + date_of_birth = cInfo.dateOfBirth, + relationship_status = cInfo.relationshipStatus, + dependants = cInfo.dependents, + dob_of_dependants = cInfo.dobOfDependents, + highest_education_attained = cInfo.highestEducationAttained, + employment_status = cInfo.employmentStatus, + kyc_status = cInfo.kycStatus, + last_ok_date = cInfo.lastOkDate ) } diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala index 9c722543a..f839ba164 100644 --- a/src/main/scala/code/customer/CustomerProvider.scala +++ b/src/main/scala/code/customer/CustomerProvider.scala @@ -19,7 +19,15 @@ trait CustomerProvider { def getUser(bankId : BankId, customerId : String) : Box[User] - def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage) : Box[Customer] + def addCustomer(bankId: BankId, user: User, number: String, legalName: String, mobileNumber: String, email: String, faceImage: CustomerFaceImage, + dateOfBirth: Date, + relationshipStatus: String, + dependents: Int, + dobOfDependents: Array[Date], + highestEducationAttained: String, + employmentStatus: String, + kycStatus: Boolean, + lastOkDate: Date): Box[Customer] } @@ -29,6 +37,14 @@ trait Customer { def mobileNumber : String def email : String def faceImage : CustomerFaceImage + def dateOfBirth: Date + def relationshipStatus: String + def dependents: Int + def dobOfDependents: Array[Date] + def highestEducationAttained: String + def employmentStatus: String + def kycStatus: Boolean + def lastOkDate: Date } trait CustomerFaceImage { diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 0134f4b9e..4de0aac35 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -23,12 +23,25 @@ object MappedCustomerProvider extends CustomerProvider { ).flatMap(_.mUser.obj) } - override def addCustomer(bankId: BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage) : Box[Customer] = { + override def addCustomer(bankId: BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage, + dateOfBirth: Date, + relationshipStatus: String, + dependents: Int, + dobOfDependents: Array[Date], + highestEducationAttained: String, + employmentStatus: String, + kycStatus: Boolean, + lastOkDate: Date) : Box[Customer] = { val createdCustomer = MappedCustomer.create .mBank(bankId.value).mEmail(email).mFaceImageTime(faceImage.date) .mFaceImageUrl(faceImage.url).mLegalName(legalName) - .mMobileNumber(mobileNumber).mNumber(number).mUser(user.apiId.value).saveMe() + .mMobileNumber(mobileNumber).mNumber(number).mUser(user.apiId.value) + .mDateOfBirth(dateOfBirth).mRelationshipStatus(relationshipStatus) + .mDependents(dependents) + .mHighestEducationAttained(highestEducationAttained) + .mEmploymentStatus(employmentStatus).mKycStatus(kycStatus) + .mLastOkDate(lastOkDate).saveMe() Some(createdCustomer) } @@ -48,6 +61,13 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with object mEmail extends MappedEmail(this, 200) object mFaceImageUrl extends DefaultStringField(this) object mFaceImageTime extends MappedDateTime(this) + object mDateOfBirth extends MappedDateTime(this) + object mRelationshipStatus extends DefaultStringField(this) + object mDependents extends MappedInt(this) + object mHighestEducationAttained extends DefaultStringField(this) + object mEmploymentStatus extends DefaultStringField(this) + object mKycStatus extends MappedBoolean(this) + object mLastOkDate extends MappedDateTime(this) override def number: String = mNumber.get override def mobileNumber: String = mMobileNumber.get @@ -57,6 +77,14 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with override def date: Date = mFaceImageTime.get override def url: String = mFaceImageUrl.get } + override def dateOfBirth: Date = mDateOfBirth.get + override def relationshipStatus: String = mRelationshipStatus.get + override def dependents: Int = mDependents + override def dobOfDependents: Array[Date] = Array(createdAt.get) + override def highestEducationAttained: String = mHighestEducationAttained.get + override def employmentStatus: String = mEmploymentStatus.get + override def kycStatus: Boolean = mKycStatus + override def lastOkDate: Date = mLastOkDate.get } object MappedCustomer extends MappedCustomer with LongKeyedMetaMapper[MappedCustomer] { diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index d1f187125..5618300f0 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -15,12 +15,16 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { val mockBankId = BankId("testBank1") case class MockFaceImage(date : Date, url : String) extends CustomerFaceImage - case class MockCustomer(number : String, mobileNumber : String, - legalName : String, email : String, - faceImage : MockFaceImage) extends Customer + + case class MockCustomer(number: String, mobileNumber: String, + legalName: String, email: String, + faceImage: MockFaceImage, dateOfBirth: Date, + relationshipStatus: String, dependents: Int, + dobOfDependents: Array[Date], highestEducationAttained: String, + employmentStatus: String, kycStatus: Boolean, lastOkDate: Date) extends Customer val mockCustomerFaceImage = MockFaceImage(new Date(1234000), "http://example.com/image1") - val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage) + val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, Array(new Date(1234000)), "Bachelor’s Degree", "Employed", true, new Date(1234000)) object MockedCustomerProvider extends CustomerProvider { override def getCustomer(bankId: BankId, user: User): Box[Customer] = { @@ -29,7 +33,15 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { } override def getUser(bankId: BankId, customerId: String): Box[User] = Empty - override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage) : Box[Customer] = Empty + override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage, + dateOfBirth: Date, + relationshipStatus: String, + dependents: Int, + dobOfDependents: Array[Date], + highestEducationAttained: String, + employmentStatus: String, + kycStatus: Boolean, + lastOkDate: Date) : Box[Customer] = Empty } override def beforeAll() { @@ -92,8 +104,19 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { And("We should get the right information back") val info = response.body.extract[CustomerJson] - val received = MockCustomer(info.customer_number, info.mobile_phone_number, - info.legal_name, info.email, MockFaceImage(info.face_image.date, info.face_image.url)) + val received = MockCustomer(info.customer_number, + info.mobile_phone_number, + info.legal_name, + info.email, + MockFaceImage(info.face_image.date, info.face_image.url), + info.date_of_birth, + info.relationship_status, + info.dependants, + info.dob_of_dependants, + info.highest_education_attained, + info.employment_status, + info.kyc_status, + info.last_ok_date) received should equal(mockCustomer) } diff --git a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala index 70b7b13bc..82c49325f 100644 --- a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala +++ b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala @@ -48,7 +48,15 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { legal_name = "Someone", mobile_phone_number = "125245", email = "hello@hullo.com", - face_image = CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate) + face_image = CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), + date_of_birth = exampleDate, + relationship_status = "Single", + dependants = 1, + dob_of_dependants = Array(exampleDate), + highest_education_attained = "Bachelor’s Degree", + employment_status = "Employed", + kyc_status = true, + last_ok_date = exampleDate ) var response = makePostRequest(request, write(customerJson)) From ed4392eafa9a327568f9a1162fb136fae11812fa Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 28 Jan 2016 15:45:04 +0100 Subject: [PATCH 315/702] Added jwtauth endpoints --- src/main/scala/bootstrap/liftweb/Boot.scala | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 93fcbac4f..922ce3037 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -31,43 +31,38 @@ Berlin 13359, Germany */ package bootstrap.liftweb +import java.io.{File, FileInputStream} +import java.util.Locale +import javax.mail.internet.MimeMessage + import code.api.ResourceDocs1_4_0.ResourceDocs +import code.api._ import code.api.sandbox.SandboxApiCalls +import code.atms.MappedAtm +import code.branches.MappedBranch import code.crm.MappedCrmEvent -import code.management.ImporterAPI -import code.management.AccountsAPI +import code.customer.{MappedCustomer, MappedCustomerMessage} +import code.management.{AccountsAPI, ImporterAPI} import code.metadata.comments.MappedComment -import code.metadata.counterparties.{MappedCounterpartyWhereTag, MappedCounterpartyMetadata} +import code.metadata.counterparties.{MappedCounterpartyMetadata, MappedCounterpartyWhereTag} import code.metadata.narrative.MappedNarrative import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag import code.metrics.MappedMetric -import code.branches.{MappedBranch} -import code.atms.{MappedAtm} -import code.customer.{MappedCustomerMessage, MappedCustomer} -import code.products.MappedProduct -import code.tesobe.CashAccountAPI -import code.transactionrequests.MappedTransactionRequest -import net.liftweb._ -import util._ -import common._ -import http._ -import sitemap._ -import Loc._ -import mapper._ -import net.liftweb.util.Helpers._ -import net.liftweb.util.Schedule -import net.liftweb.util.Helpers -import java.util.Locale -import java.io.FileInputStream -import java.io.File -import javax.mail.internet.MimeMessage import code.model._ import code.model.dataAccess._ -import code.api._ +import code.products.MappedProduct import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} -import code.api.ResourceDocs1_4_0.{ResourceDocsAPIMethods} +import code.tesobe.CashAccountAPI +import code.transactionrequests.MappedTransactionRequest +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.mapper._ +import net.liftweb.sitemap.Loc._ +import net.liftweb.sitemap._ +import net.liftweb.util.Helpers._ +import net.liftweb.util.{Helpers, Schedule, _} @@ -186,6 +181,11 @@ class Boot extends Loggable{ //OAuth API call LiftRules.statelessDispatch.append(OAuthHandshake) + // JWT auth endpoints + if(Props.getBool("allow_jwt_auth", true)) { + LiftRules.statelessDispatch.append(JWTAuth) + } + // Add the various API versions LiftRules.statelessDispatch.append(v1_0.OBPAPI1_0) LiftRules.statelessDispatch.append(v1_1.OBPAPI1_1) @@ -338,8 +338,8 @@ class Boot extends Loggable{ } private def sendExceptionEmail(exception: Throwable): Unit = { + import Mailer.{From, PlainMailBodyType, Subject, To} import net.liftweb.util.Helpers.now - import Mailer.{From, To, Subject, PlainMailBodyType} val outputStream = new java.io.ByteArrayOutputStream val printStream = new java.io.PrintStream(outputStream) From 074facd799e3c817c0dc2791c6e1e1f0a425a79e Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 28 Jan 2016 15:46:39 +0100 Subject: [PATCH 316/702] Jwtauth endpoints --- src/main/scala/code/api/jwtauth.scala | 459 ++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 src/main/scala/code/api/jwtauth.scala diff --git a/src/main/scala/code/api/jwtauth.scala b/src/main/scala/code/api/jwtauth.scala new file mode 100644 index 000000000..a020e9681 --- /dev/null +++ b/src/main/scala/code/api/jwtauth.scala @@ -0,0 +1,459 @@ +/** +Open Bank Project + +Copyright 2011,2012 TESOBE / Music Pictures Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + Open Bank Project (http://www.openbankproject.com) + Copyright 2011,2012 TESOBE / Music Pictures Ltd + + This product includes software developed at + TESOBE (http://www.tesobe.com/) + by + Simon Redfern : simon AT tesobe DOT com + Everett Sochowski: everett AT tesobe DOT com + Ayoub Benali : ayoub AT tesobe Dot com + */ +package code.api + +import java.net.{URLDecoder, URLEncoder} +import java.util.Date +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +import authentikat.jwt._ +import code.model.{Consumer, Token, TokenType, User} +import net.liftweb.common.{Box, Empty, Full, Loggable, ParamFailure} +import net.liftweb.http._ +import net.liftweb.http.rest.RestHelper +import net.liftweb.mapper.By +import net.liftweb.util.Helpers.{tryo, _} +import net.liftweb.util.{Helpers, Props} + +import scala.compat.Platform + +/** +* This object provides the API calls necessary to third party applications +* so they could authenticate their users. +*/ + +object JWTAuth extends RestHelper with Loggable { + serve + { + //Handling get request for a "request token" + case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest) => + { + //Extract the jwtauth parameters from the header and test if the request is valid + var (httpCode, message, jwtAuthParameters) = validator("requestToken", "POST") + //Test if the request is valid + if(httpCode==200) + { + //Generate the token and secret + val secret = Helpers.randomString(40) + val header = JwtHeader("HS256") + val claimsSet = JwtClaimsSet(Map("app" -> "key")) + val token: String = JsonWebToken(header, claimsSet, secret) + + //Save the token that we have generated + if(saveRequestToken(jwtAuthParameters,token, secret)) + message="token="+token + } + val headers = ("Content-type" -> "application/x-www-form-urlencoded") :: Nil + //return an HTTP response + Full(InMemoryResponse(message.getBytes,headers,Nil,httpCode)) + } + + case Req("my" :: "logins" :: "authorize" :: Nil,_, PostRequest) => authorize() + case Req("my" :: "logins" :: "authorize" :: Nil,_, GetRequest) => authorize() + + } + + def authorize() = + { + //Extract the jwtauth parameters from the header and test if the request is valid + var (httpCode, message, jwtAuthParameters) = validator("authorizationToken", "POST") + //Test if the request is valid + if(httpCode==200) + { + //Generate the token and secret/token + val (token,secret) = generateTokenAndSecret() + //Save the token that we have generated + if(saveAuthorizationToken(jwtAuthParameters,token, secret)) + //remove the request token so the application could not exchange it + //again to get an other access token + Token.find(By(Token.key,jwtAuthParameters.get("token").get)) match { + case Full(requestToken) => requestToken.delete_! + case _ => None + } + + message="token="+token+"&token_secret="+secret + } + val headers = ("Content-type" -> "application/x-www-form-urlencoded") :: Nil + //return an HTTP response + Full(InMemoryResponse(message.getBytes,headers,Nil,httpCode)) + } + + //Check if the request (access token or request token) is valid and return a tuple + def validator(requestType : String, httpMethod : String) : (Int, String, Map[String,String]) = { + //return a Map containing the jwtauth parameters : prameter -> value + def getAllParameters : Map[String,String]= { + + def toMapFromBasicAuth(paramsEncoded : String) = { + val params = new String(base64Decode(paramsEncoded.replaceAll("Basic ", ""))) + params.toString.split(":") match { + case Array(str1, str2) => Map("username" -> str1, "password" -> str2) + case _ => Map("error" -> "error") + } + } + + //Convert the list of jwtauth parameters to a Map + def toMap(parametersList : Map[String, List[String]]) = { + println("parameters=" + parametersList) + + val jwtauthPossibleParameters = + List( + "username", + "password", + "token" + ) + for ( p <- parametersList if jwtauthPossibleParameters.contains(p._1)) + yield (p._1 -> p._2.head) + } + + S.request match { + case Full(a) => a.header("Authorization") match { + case Full(parameters) => toMapFromBasicAuth(parameters) + case _ => toMap(a.params) + } + case _ => Map(("","")) + } + } + + def supportedjwtauthVersion(jwtauthVersion : Option[String]) : Boolean = { + //auth_version is OPTIONAL. If present, MUST be set to "1.0". + jwtauthVersion match + { + case Some(a) => a=="1" || a=="1.0" + case _ => true + } + } + + def wrongTimestamp(requestTimestamp : Option[String]) : Option[String] = { + requestTimestamp match { + case Some(timestamp) => { + tryo{ + timestamp.toLong + } match { + case Full(l) => + tryo{ + new Date(l) + } match { + case Full(d) => { + val currentTime = Platform.currentTime / 1000 + val timeRange : Long = Helpers.minutes(3) + //check if the timestamp is positive and in the time range + if(d.getTime < 0 || d.before(new Date(currentTime - timeRange)) || d.after(new Date(currentTime + timeRange))) + Some("timestamp value: " + timestamp +" is in and invalid time range") + else + None + } + case _ => Some("timestamp value: " + timestamp +" is an invalid date") + } + case _ => Some("timestamp value: " + timestamp +" is invalid") + } + + } + case _ => Some("the following parameter is missing: timestamp") + } + } + + def registeredApplication(consumerKey : String ) : Boolean = { + Consumer.find(By(Consumer.key,consumerKey)) match { + case Full(application) => application.isActive + case _ => false + } + } + + def correctSignature(jwtauthparameters : Map[String, String], httpMethod : String) = { + //Normalize an encode the request parameters as explained in Section 3.4.1.3.2 + //of jwtauth 1.0 specification (http://tools.ietf.org/html/rfc5849) + def generatejwtauthParametersString(jwtauthparameters : Map[String, String]) : String = { + def sortParam( keyAndValue1 : (String, String), keyAndValue2 : (String, String))= keyAndValue1._1.compareTo(keyAndValue2._1) < 0 + var parameters ="" + + //sort the parameters by name + jwtauthparameters.toList.sortWith(sortParam _).foreach( + t => + if(t._1 != "signature") + parameters += URLEncoder.encode(t._1,"UTF-8")+"%3D"+ URLEncoder.encode(t._2,"UTF-8")+"%26" + + ) + parameters = parameters.dropRight(3) //remove the "&" encoded sign + parameters + } + + //prepare the base string + var baseString = httpMethod+"&"+URLEncoder.encode(Props.get("hostname").openOr(S.hostAndPath) + S.uri,"UTF-8")+"&" + baseString+= generatejwtauthParametersString(jwtauthparameters) + + val encodeBaseString = URLEncoder.encode(baseString,"UTF-8") + //get the key to sign + val comsumer = Consumer.find( + By(Consumer.key,jwtauthparameters.get("consumer_key").get) + ).get + var secret= comsumer.secret.toString + + jwtauthparameters.get("token") match { + case Some(tokenKey) => Token.find(By(Token.key,tokenKey)) match { + case Full(token) => secret+= "&" +token.secret.toString() + case _ => secret+= "&" + } + case _ => secret+= "&" + } + logger.info("base string: " + baseString) + //signing process + val signingAlgorithm : String = if(jwtauthparameters.get("signature_method").get.toLowerCase == "hmac-sha256") + "HmacSHA256" + else + "HmacSHA1" + + logger.info("signing method: " + signingAlgorithm) + logger.info("signing key: " + secret) + logger.info("signing key in bytes: " + secret.getBytes("UTF-8")) + + var m = Mac.getInstance(signingAlgorithm); + m.init(new SecretKeySpec(secret.getBytes("UTF-8"),signingAlgorithm)) + val calculatedSignature = Helpers.base64Encode(m.doFinal(baseString.getBytes)) + + logger.info("calculatedSignature: " + calculatedSignature) + //logger.info("received signature:" + jwtauthparameters.get("signature").get) + logger.info("received signature after decoding: " + URLDecoder.decode(jwtauthparameters.get("signature").get)) + + calculatedSignature== URLDecoder.decode(jwtauthparameters.get("signature").get,"UTF-8") + } + + //check if the token exists and is still valid + def validToken(tokenKey : String) ={ + Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Request)) match { + case Full(token) => token.isValid + case _ => false + } + } + + def validToken2(tokenKey : String) = { + Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Access)) match { + case Full(token) => token.isValid + case _ => false + } + } + + //@return the missing parameters depending of the request type + def missingjwtauthParameters(parameters : Map[String, String], requestType : String) : Set[String] = { + val parametersBase = + List() + // "username", + // "password", + // "token" + // ) + if(requestType == "requestToken") + ("username" :: "password" :: parametersBase).toSet diff parameters.keySet + else if(requestType=="authorizationToken") + ("token" :: parametersBase).toSet diff parameters.keySet + else if(requestType=="protectedResource") + ("token" :: parametersBase).toSet diff parameters.keySet + else + parameters.keySet + } + + def supportedSignatureMethod(jwtauthSignatureMethod : String ) : Boolean = + { + jwtauthSignatureMethod.toLowerCase == "hmac-sha256" || + jwtauthSignatureMethod.toLowerCase == "hs256" + } + + var message = "" + var httpCode : Int = 500 + + var parameters = getAllParameters + + //are all the necessary jwtauth parameters present? + val missingParams = missingjwtauthParameters(parameters,requestType) + if( missingParams.size != 0 ) + { + message = "the following parameters are missing : " + missingParams.mkString(", ") + httpCode = 400 + } + + //supported signature method + //else if (!supportedSignatureMethod(parameters.get("signature_method").get)) + //{ + // message = "Unsupported signature method, please use hmac-sha1 or hmac-sha256" + // httpCode = 400 + //} + //check if the application is registered and active + //else if(! registeredApplication(parameters.get("consumer_key").get)) + //{ + // logger.error("application: " + parameters.get("consumer_key").get + " not found") + // message = "Invalid consumer credentials" + // httpCode = 401 + //} + //valid timestamp + //else if(! wrongTimestamp(parameters.get("timestamp")).isEmpty) + //{ + // message = wrongTimestamp(parameters.get("timestamp")).get + // httpCode = 400 + //} + + //In the case jwtauth authorization token request, check if the token is still valid and the verifier is correct + else if(requestType=="authorizationToken" && !validToken(parameters.get("token").get)) + { + message = "Invalid or expired request token: " + parameters.get("token").get + httpCode = 401 + } + //In the case protected resource access request, check if the token is still valid + else if ( + requestType=="protectedResource" && + ! validToken2(parameters.get("token").get) + ) + { + message = "Invalid or expired access token: " + parameters.get("token").get + httpCode = 401 + } + //checking if the signature is correct + //else if(! correctSignature(parameters, httpMethod)) + //{ + // message = "Invalid signature" + // httpCode = 401 + //} + else + httpCode = 200 + if(message.nonEmpty) + logger.error("error message : " + message) + + (httpCode, message, parameters) + } + + private def generateTokenAndSecret() = + { + // generate some random strings + val token_message = Helpers.randomString(40) + val secret_message = Helpers.randomString(40) + + (token_message, secret_message) + } + + private def saveRequestToken(jwtauthParameters : Map[String, String], tokenKey : String, tokenSecret : String) = + { + import code.model.{Token, TokenType} + + //val nonce = Nonce.create + //nonce.consumerkey(jwtauthParameters.get("consumer_key").get) + //nonce.timestamp(new Date(jwtauthParameters.get("timestamp").get.toLong)) + //nonce.value(jwtauthParameters.get("nonce").get) + //val nonceSaved = nonce.save() + + val token = Token.create + token.tokenType(TokenType.Request) + //if(! jwtauthParameters.get("consumer_key").get.isEmpty) + // Consumer.find(By(Consumer.key,jwtauthParameters.get("consumer_key").get)) match { + // case Full(consumer) => token.consumerId(consumer.id) + // case _ => None + //} + token.key(tokenKey) + token.secret(tokenSecret) + //if(! jwtauthParameters.get("callback").get.isEmpty) + // token.callbackURL(URLDecoder.decode(jwtauthParameters.get("callback").get,"UTF-8")) + //else + token.callbackURL("oob") + val currentTime = Platform.currentTime + val tokenDuration : Long = Helpers.minutes(30) + token.duration(tokenDuration) + token.expirationDate(new Date(currentTime+tokenDuration)) + token.insertDate(new Date(currentTime)) + val tokenSaved = token.save() + + // nonceSaved && tokenSaved + true && tokenSaved + } + + private def saveAuthorizationToken(jwtauthParameters : Map[String, String], tokenKey : String, tokenSecret : String) = + { + import code.model.{Token, TokenType} + + //val nonce = Nonce.create + //nonce.consumerkey(jwtauthParameters.get("consumer_key").get) + //nonce.timestamp(new Date(jwtauthParameters.get("timestamp").get.toLong)) + //nonce.tokenKey(jwtauthParameters.get("token").get) + //nonce.value(jwtauthParameters.get("nonce").get) + //val nonceSaved = nonce.save() + + val token = Token.create + token.tokenType(TokenType.Access) + //Consumer.find(By(Consumer.key,jwtauthParameters.get("consumer_key").get)) match { + // case Full(consumer) => token.consumerId(consumer.id) + // case _ => None + //} + Token.find(By(Token.key, jwtauthParameters.get("token").get)) match { + case Full(requestToken) => token.userForeignKey(requestToken.userForeignKey) + case _ => None + } + token.key(tokenKey) + token.secret(tokenSecret) + val currentTime = Platform.currentTime + val tokenDuration : Long = Helpers.weeks(4) + token.duration(tokenDuration) + token.expirationDate(new Date(currentTime+tokenDuration)) + token.insertDate(new Date(currentTime)) + val tokenSaved = token.save() + + //nonceSaved && + tokenSaved + } + + def getUser : Box[User] = { + val httpMethod = S.request match { + case Full(r) => r.request.method + case _ => "GET" + } + val (httpCode, message, jwtauthParameters) = validator("protectedResource", httpMethod) + + if(httpCode== 200) getUser(httpCode, jwtauthParameters.get("token")) + else ParamFailure(message, Empty, Empty, APIFailure(message, httpCode)) + } + + def getUser(httpCode : Int, tokenID : Box[String]) : Box[User] = + if(httpCode==200) + { + import code.model.Token + logger.info("jwtauth header correct ") + Token.find(By(Token.key, tokenID.get)) match { + case Full(token) => { + logger.info("access token: "+ token + " found") + val user = token.user + //just a log + user match { + case Full(u) => logger.info("user " + u.emailAddress + " was found from the jwtauth token") + case _ => logger.info("no user was found for the jwtauth token") + } + user + } + case _ =>{ + logger.warn("no token " + tokenID.get + " found") + Empty + } + } + } + else + Empty +} \ No newline at end of file From a99a78f532c8639b1ec30aafefb8000f781dfd97 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 28 Jan 2016 17:06:53 +0100 Subject: [PATCH 317/702] Array replaced by List - Add new fields to the Customer --- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 2 +- src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala | 2 +- src/main/scala/code/customer/CustomerProvider.scala | 4 ++-- src/main/scala/code/customer/MappedCustomerProvider.scala | 4 ++-- src/test/scala/code/api/v1_4_0/CustomerTest.scala | 8 ++++---- .../code/api/v1_4_0/MappedCustomerMessagesTest.scala | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index fe6e0b4a1..6dcc4652a 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -500,7 +500,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |""", Extraction.decompose(CustomerJson("687687678", "Joe David Bloggs", "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), - exampleDate, "Single", 1, Array(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)), + exampleDate, "Single", 1, List(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index bb0f33325..a69714788 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -27,7 +27,7 @@ object JSONFactory1_4_0 { date_of_birth: Date, relationship_status: String, dependants: Int, - dob_of_dependants: Array[Date], + dob_of_dependants: List[Date], highest_education_attained: String, employment_status: String, kyc_status: Boolean, diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala index f839ba164..785217192 100644 --- a/src/main/scala/code/customer/CustomerProvider.scala +++ b/src/main/scala/code/customer/CustomerProvider.scala @@ -23,7 +23,7 @@ trait CustomerProvider { dateOfBirth: Date, relationshipStatus: String, dependents: Int, - dobOfDependents: Array[Date], + dobOfDependents: List[Date], highestEducationAttained: String, employmentStatus: String, kycStatus: Boolean, @@ -40,7 +40,7 @@ trait Customer { def dateOfBirth: Date def relationshipStatus: String def dependents: Int - def dobOfDependents: Array[Date] + def dobOfDependents: List[Date] def highestEducationAttained: String def employmentStatus: String def kycStatus: Boolean diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 4de0aac35..4ee76ebb1 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -27,7 +27,7 @@ object MappedCustomerProvider extends CustomerProvider { dateOfBirth: Date, relationshipStatus: String, dependents: Int, - dobOfDependents: Array[Date], + dobOfDependents: List[Date], highestEducationAttained: String, employmentStatus: String, kycStatus: Boolean, @@ -80,7 +80,7 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with override def dateOfBirth: Date = mDateOfBirth.get override def relationshipStatus: String = mRelationshipStatus.get override def dependents: Int = mDependents - override def dobOfDependents: Array[Date] = Array(createdAt.get) + override def dobOfDependents: List[Date] = List(createdAt.get) override def highestEducationAttained: String = mHighestEducationAttained.get override def employmentStatus: String = mEmploymentStatus.get override def kycStatus: Boolean = mKycStatus diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index 5618300f0..e602c9076 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -20,11 +20,11 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { legalName: String, email: String, faceImage: MockFaceImage, dateOfBirth: Date, relationshipStatus: String, dependents: Int, - dobOfDependents: Array[Date], highestEducationAttained: String, + dobOfDependents: List[Date], highestEducationAttained: String, employmentStatus: String, kycStatus: Boolean, lastOkDate: Date) extends Customer val mockCustomerFaceImage = MockFaceImage(new Date(1234000), "http://example.com/image1") - val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, Array(new Date(1234000)), "Bachelor’s Degree", "Employed", true, new Date(1234000)) + val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(new Date(1234000), new Date(1234000), new Date(1234000)), "Bachelor’s Degree", "Employed", true, new Date(1234000)) object MockedCustomerProvider extends CustomerProvider { override def getCustomer(bankId: BankId, user: User): Box[Customer] = { @@ -37,7 +37,7 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { dateOfBirth: Date, relationshipStatus: String, dependents: Int, - dobOfDependents: Array[Date], + dobOfDependents: List[Date], highestEducationAttained: String, employmentStatus: String, kycStatus: Boolean, @@ -112,7 +112,7 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { info.date_of_birth, info.relationship_status, info.dependants, - info.dob_of_dependants, + info.dob_of_dependants.sortBy(_.getTime), info.highest_education_attained, info.employment_status, info.kyc_status, diff --git a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala index 82c49325f..4020e79c3 100644 --- a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala +++ b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala @@ -52,7 +52,7 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { date_of_birth = exampleDate, relationship_status = "Single", dependants = 1, - dob_of_dependants = Array(exampleDate), + dob_of_dependants = List(exampleDate), highest_education_attained = "Bachelor’s Degree", employment_status = "Employed", kyc_status = true, From 602fa5f53490478aed1ecc417e04863b1db74efa Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 28 Jan 2016 17:10:49 +0100 Subject: [PATCH 318/702] Jwtauth endpoints. experimental code at the moment. --- src/main/scala/code/api/jwtauth.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/jwtauth.scala b/src/main/scala/code/api/jwtauth.scala index a020e9681..afe476910 100644 --- a/src/main/scala/code/api/jwtauth.scala +++ b/src/main/scala/code/api/jwtauth.scala @@ -136,7 +136,7 @@ object JWTAuth extends RestHelper with Loggable { case Full(parameters) => toMapFromBasicAuth(parameters) case _ => toMap(a.params) } - case _ => Map(("","")) + case _ => toMap(S.request.openTheBox.params) } } @@ -232,7 +232,7 @@ object JWTAuth extends RestHelper with Loggable { logger.info("signing key: " + secret) logger.info("signing key in bytes: " + secret.getBytes("UTF-8")) - var m = Mac.getInstance(signingAlgorithm); + var m = Mac.getInstance(signingAlgorithm) m.init(new SecretKeySpec(secret.getBytes("UTF-8"),signingAlgorithm)) val calculatedSignature = Helpers.base64Encode(m.doFinal(baseString.getBytes)) @@ -266,6 +266,7 @@ object JWTAuth extends RestHelper with Loggable { // "password", // "token" // ) + println(parameters.toString) if(requestType == "requestToken") ("username" :: "password" :: parametersBase).toSet diff parameters.keySet else if(requestType=="authorizationToken") From c8c82f3123f8a061e96df175b2793ab8349b7f41 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 29 Jan 2016 09:27:10 +0100 Subject: [PATCH 319/702] Code cleanup - experimental code at the moment --- src/main/scala/code/api/jwtauth.scala | 201 +++++--------------------- 1 file changed, 39 insertions(+), 162 deletions(-) diff --git a/src/main/scala/code/api/jwtauth.scala b/src/main/scala/code/api/jwtauth.scala index afe476910..57618aa7f 100644 --- a/src/main/scala/code/api/jwtauth.scala +++ b/src/main/scala/code/api/jwtauth.scala @@ -27,19 +27,16 @@ limitations under the License. */ package code.api -import java.net.{URLDecoder, URLEncoder} import java.util.Date -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import authentikat.jwt._ +import authentikat.jwt.{JwtHeader, JsonWebToken, JwtClaimsSet} import code.model.{Consumer, Token, TokenType, User} import net.liftweb.common.{Box, Empty, Full, Loggable, ParamFailure} import net.liftweb.http._ import net.liftweb.http.rest.RestHelper import net.liftweb.mapper.By -import net.liftweb.util.Helpers.{tryo, _} -import net.liftweb.util.{Helpers, Props} +import net.liftweb.util.Helpers +import net.liftweb.util.Helpers._ import scala.compat.Platform @@ -59,11 +56,8 @@ object JWTAuth extends RestHelper with Loggable { //Test if the request is valid if(httpCode==200) { - //Generate the token and secret - val secret = Helpers.randomString(40) - val header = JwtHeader("HS256") - val claimsSet = JwtClaimsSet(Map("app" -> "key")) - val token: String = JsonWebToken(header, claimsSet, secret) + val claims = Map("app" -> "key") + val (token,secret) = generateTokenAndSecret(claims) //Save the token that we have generated if(saveRequestToken(jwtAuthParameters,token, secret)) @@ -87,7 +81,8 @@ object JWTAuth extends RestHelper with Loggable { if(httpCode==200) { //Generate the token and secret/token - val (token,secret) = generateTokenAndSecret() + val claims = Map("app" -> "key") + val (token,secret) = generateTokenAndSecret(claims) //Save the token that we have generated if(saveAuthorizationToken(jwtAuthParameters,token, secret)) //remove the request token so the application could not exchange it @@ -110,6 +105,7 @@ object JWTAuth extends RestHelper with Loggable { def getAllParameters : Map[String,String]= { def toMapFromBasicAuth(paramsEncoded : String) = { + println("basic=" + paramsEncoded ) val params = new String(base64Decode(paramsEncoded.replaceAll("Basic ", ""))) params.toString.split(":") match { case Array(str1, str2) => Map("username" -> str1, "password" -> str2) @@ -118,63 +114,31 @@ object JWTAuth extends RestHelper with Loggable { } //Convert the list of jwtauth parameters to a Map - def toMap(parametersList : Map[String, List[String]]) = { - println("parameters=" + parametersList) + def toMapFromReq(parametersList : Req ) = { + val jwtauthPossibleParameters = + List( + "username", + "password", + "token" + ) - val jwtauthPossibleParameters = - List( - "username", - "password", - "token" - ) - for ( p <- parametersList if jwtauthPossibleParameters.contains(p._1)) - yield (p._1 -> p._2.head) + if (parametersList.json_?) { + val parameters = parametersList.json.map( _ .values ).getOrElse(Map[String,String] _) + parameters.asInstanceOf[Map[String,String]] + } else if (parametersList.xml_?) { + val parameters = parametersList.xml.map( _ .text ).getOrElse(Map[String,String] _) + parameters.asInstanceOf[Map[String,String]] + } else { + Map("error" -> "parameters incorrect") + } } S.request match { case Full(a) => a.header("Authorization") match { case Full(parameters) => toMapFromBasicAuth(parameters) - case _ => toMap(a.params) + case _ => toMapFromReq(a) } - case _ => toMap(S.request.openTheBox.params) - } - } - - def supportedjwtauthVersion(jwtauthVersion : Option[String]) : Boolean = { - //auth_version is OPTIONAL. If present, MUST be set to "1.0". - jwtauthVersion match - { - case Some(a) => a=="1" || a=="1.0" - case _ => true - } - } - - def wrongTimestamp(requestTimestamp : Option[String]) : Option[String] = { - requestTimestamp match { - case Some(timestamp) => { - tryo{ - timestamp.toLong - } match { - case Full(l) => - tryo{ - new Date(l) - } match { - case Full(d) => { - val currentTime = Platform.currentTime / 1000 - val timeRange : Long = Helpers.minutes(3) - //check if the timestamp is positive and in the time range - if(d.getTime < 0 || d.before(new Date(currentTime - timeRange)) || d.after(new Date(currentTime + timeRange))) - Some("timestamp value: " + timestamp +" is in and invalid time range") - else - None - } - case _ => Some("timestamp value: " + timestamp +" is an invalid date") - } - case _ => Some("timestamp value: " + timestamp +" is invalid") - } - - } - case _ => Some("the following parameter is missing: timestamp") + case _ => Map("error" -> "request incorrect") } } @@ -185,73 +149,15 @@ object JWTAuth extends RestHelper with Loggable { } } - def correctSignature(jwtauthparameters : Map[String, String], httpMethod : String) = { - //Normalize an encode the request parameters as explained in Section 3.4.1.3.2 - //of jwtauth 1.0 specification (http://tools.ietf.org/html/rfc5849) - def generatejwtauthParametersString(jwtauthparameters : Map[String, String]) : String = { - def sortParam( keyAndValue1 : (String, String), keyAndValue2 : (String, String))= keyAndValue1._1.compareTo(keyAndValue2._1) < 0 - var parameters ="" - - //sort the parameters by name - jwtauthparameters.toList.sortWith(sortParam _).foreach( - t => - if(t._1 != "signature") - parameters += URLEncoder.encode(t._1,"UTF-8")+"%3D"+ URLEncoder.encode(t._2,"UTF-8")+"%26" - - ) - parameters = parameters.dropRight(3) //remove the "&" encoded sign - parameters - } - - //prepare the base string - var baseString = httpMethod+"&"+URLEncoder.encode(Props.get("hostname").openOr(S.hostAndPath) + S.uri,"UTF-8")+"&" - baseString+= generatejwtauthParametersString(jwtauthparameters) - - val encodeBaseString = URLEncoder.encode(baseString,"UTF-8") - //get the key to sign - val comsumer = Consumer.find( - By(Consumer.key,jwtauthparameters.get("consumer_key").get) - ).get - var secret= comsumer.secret.toString - - jwtauthparameters.get("token") match { - case Some(tokenKey) => Token.find(By(Token.key,tokenKey)) match { - case Full(token) => secret+= "&" +token.secret.toString() - case _ => secret+= "&" - } - case _ => secret+= "&" - } - logger.info("base string: " + baseString) - //signing process - val signingAlgorithm : String = if(jwtauthparameters.get("signature_method").get.toLowerCase == "hmac-sha256") - "HmacSHA256" - else - "HmacSHA1" - - logger.info("signing method: " + signingAlgorithm) - logger.info("signing key: " + secret) - logger.info("signing key in bytes: " + secret.getBytes("UTF-8")) - - var m = Mac.getInstance(signingAlgorithm) - m.init(new SecretKeySpec(secret.getBytes("UTF-8"),signingAlgorithm)) - val calculatedSignature = Helpers.base64Encode(m.doFinal(baseString.getBytes)) - - logger.info("calculatedSignature: " + calculatedSignature) - //logger.info("received signature:" + jwtauthparameters.get("signature").get) - logger.info("received signature after decoding: " + URLDecoder.decode(jwtauthparameters.get("signature").get)) - - calculatedSignature== URLDecoder.decode(jwtauthparameters.get("signature").get,"UTF-8") - } - //check if the token exists and is still valid - def validToken(tokenKey : String) ={ + def validRequestToken(tokenKey : String) ={ Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Request)) match { case Full(token) => token.isValid case _ => false } } - def validToken2(tokenKey : String) = { + def validAccessToken(tokenKey : String) = { Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Access)) match { case Full(token) => token.isValid case _ => false @@ -260,29 +166,17 @@ object JWTAuth extends RestHelper with Loggable { //@return the missing parameters depending of the request type def missingjwtauthParameters(parameters : Map[String, String], requestType : String) : Set[String] = { - val parametersBase = - List() - // "username", - // "password", - // "token" - // ) println(parameters.toString) if(requestType == "requestToken") - ("username" :: "password" :: parametersBase).toSet diff parameters.keySet + ("username" :: "password" :: List()).toSet diff parameters.keySet else if(requestType=="authorizationToken") - ("token" :: parametersBase).toSet diff parameters.keySet + ("token" :: List()).toSet diff parameters.keySet else if(requestType=="protectedResource") - ("token" :: parametersBase).toSet diff parameters.keySet + ("token" :: List()).toSet diff parameters.keySet else parameters.keySet } - def supportedSignatureMethod(jwtauthSignatureMethod : String ) : Boolean = - { - jwtauthSignatureMethod.toLowerCase == "hmac-sha256" || - jwtauthSignatureMethod.toLowerCase == "hs256" - } - var message = "" var httpCode : Int = 500 @@ -296,28 +190,8 @@ object JWTAuth extends RestHelper with Loggable { httpCode = 400 } - //supported signature method - //else if (!supportedSignatureMethod(parameters.get("signature_method").get)) - //{ - // message = "Unsupported signature method, please use hmac-sha1 or hmac-sha256" - // httpCode = 400 - //} - //check if the application is registered and active - //else if(! registeredApplication(parameters.get("consumer_key").get)) - //{ - // logger.error("application: " + parameters.get("consumer_key").get + " not found") - // message = "Invalid consumer credentials" - // httpCode = 401 - //} - //valid timestamp - //else if(! wrongTimestamp(parameters.get("timestamp")).isEmpty) - //{ - // message = wrongTimestamp(parameters.get("timestamp")).get - // httpCode = 400 - //} - //In the case jwtauth authorization token request, check if the token is still valid and the verifier is correct - else if(requestType=="authorizationToken" && !validToken(parameters.get("token").get)) + else if(requestType=="authorizationToken" && !validRequestToken(parameters.get("token").get)) { message = "Invalid or expired request token: " + parameters.get("token").get httpCode = 401 @@ -325,7 +199,7 @@ object JWTAuth extends RestHelper with Loggable { //In the case protected resource access request, check if the token is still valid else if ( requestType=="protectedResource" && - ! validToken2(parameters.get("token").get) + ! validAccessToken(parameters.get("token").get) ) { message = "Invalid or expired access token: " + parameters.get("token").get @@ -345,11 +219,14 @@ object JWTAuth extends RestHelper with Loggable { (httpCode, message, parameters) } - private def generateTokenAndSecret() = + private def generateTokenAndSecret(claims: Map[String,String]) = { - // generate some random strings - val token_message = Helpers.randomString(40) + // generate random string val secret_message = Helpers.randomString(40) + // jwt header + val header = JwtHeader("HS256") + // generate jwt token + val token_message = JsonWebToken(header, JwtClaimsSet(claims), secret_message) (token_message, secret_message) } From 515160135952de2469a2cd42c863c6652bfe7ba6 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 29 Jan 2016 23:03:30 +0100 Subject: [PATCH 320/702] Modified login --- .../scala/code/model/dataAccess/OBPUser.scala | 135 ++++++++++-------- 1 file changed, 79 insertions(+), 56 deletions(-) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 96ffa497e..3724c1a02 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -292,6 +292,74 @@ import net.liftweb.util.Helpers._ S.error("login", S.?("Invalid Username or Password")) } + def getUserId(username: String, password: String): Long = { + findUserByUserName(username) match { + case Full(user) => { + if (user.validated_? && + user.getProvider() == Props.get("hostname","") && + user.testPassword(Full(password))) + { + user.id.toLong + } + else { + getExternalUser(username, password).get.id.toLong + } + } + case _ => 0 + } + } + + def getExternalUser(username: String, password: String):Box[OBPUser] = { + getUserViaKafka(username, password) match { + case Full(SandboxUserImport(extEmail, extPassword, extDisplayName)) => { + val preLoginState = capturePreLoginState() + info("external user authenticated. login redir: " + loginRedirect.get) + val redir = loginRedirect.get match { + case Full(url) => + loginRedirect(Empty) + url + case _ => + homePage + } + + val dummyPassword = "nothingreallyjustdummypass" + val extProvider = Props.get("connector").openOrThrowException("no connector set") + + val user = findUserByUserName(username) match { + // Check if the external user is already created locally + case Full(user) if user.validated_? && + user.provider == extProvider => { + // Return existing user if found + info("external user already exists locally, using that one") + user + } + + // If not found, create new user + case _ => { + // Create OBPUser using fetched data from Kafka + // assuming that user's email is always validated + info("external user does not exist locally, creating one") + val newUser = OBPUser.create + .firstName(extDisplayName) + .email(extEmail) + // No need to store password, so store dummy string instead + .password(dummyPassword) + .provider(extProvider) + .validated(true) + // Save the user in order to be able to log in + newUser.save() + // Return created user + newUser + } + } + Full(user) + } + case _ => { + Empty + } + } + } + //overridden to allow a redirection if login fails override def login = { if (S.post_?) { @@ -326,67 +394,22 @@ import net.liftweb.util.Helpers._ case _ => { // If not found locally, try to authenticate user via Kafka, if enabled in props if (Props.get("connector").openOrThrowException("no connector set") == "kafka") { - S.param("username"). - flatMap(username => getUserViaKafka(username, S.param("password").openOr(""))) match { - case Full(SandboxUserImport(extEmail, extPassword, extDisplayName)) => { - val preLoginState = capturePreLoginState() - info("external user authenticated. login redir: " + loginRedirect.get) - val redir = loginRedirect.get match { - case Full(url) => - loginRedirect(Empty) - url - case _ => - homePage - } + val preLoginState = capturePreLoginState() + val user = getExternalUser(S.param("username").get, S.param("password").get) - val dummyPassword = "nothingreallyjustdummypass" - val extProvider = Props.get("connector").openOrThrowException("no connector set") + if (!user.isEmpty) { + logUserIn(user.get, () => { + S.notice(S.?("logged.in")) - val user = S.param("username"). - flatMap(username => findUserByUserName(username)) match { + preLoginState() - // Check if the external user is already created locally - case Full(user) if user.validated_? && - user.provider == extProvider => { - // Return existing user if found - info("external user already exists locally, using that one") - user - } - - // If not found, create new user - case _ => { - // Create OBPUser using fetched data from Kafka - // assuming that user's email is always validated - info("external user does not exist locally, creating one") - val newUser = OBPUser.create - .firstName(extDisplayName) - .email(extEmail) - // No need to store password, so store dummy string instead - .password(dummyPassword) - .provider(extProvider) - .validated(true) - // Save the user in order to be able to log in - newUser.save() - // Return created user - newUser - } - } - - logUserIn(user, () => { - S.notice(S.?("logged.in")) - - preLoginState() - - S.redirectTo(homePage) - }) - - } - case _ => { - userLoginFailed - } + S.redirectTo(homePage) + }) + } else { + userLoginFailed } } else { - userLoginFailed + userLoginFailed } } } From 4f43160f7d2a3431887dfd0435386c8b103273e2 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 29 Jan 2016 23:09:13 +0100 Subject: [PATCH 321/702] Code cleanup --- src/main/scala/code/api/jwtauth.scala | 115 +++++++------------------- 1 file changed, 31 insertions(+), 84 deletions(-) diff --git a/src/main/scala/code/api/jwtauth.scala b/src/main/scala/code/api/jwtauth.scala index 57618aa7f..9579b53a3 100644 --- a/src/main/scala/code/api/jwtauth.scala +++ b/src/main/scala/code/api/jwtauth.scala @@ -29,9 +29,10 @@ package code.api import java.util.Date -import authentikat.jwt.{JwtHeader, JsonWebToken, JwtClaimsSet} +import authentikat.jwt.{JsonWebToken, JwtClaimsSet, JwtHeader} +import code.model.dataAccess.OBPUser import code.model.{Consumer, Token, TokenType, User} -import net.liftweb.common.{Box, Empty, Full, Loggable, ParamFailure} +import net.liftweb.common._ import net.liftweb.http._ import net.liftweb.http.rest.RestHelper import net.liftweb.mapper.By @@ -48,11 +49,11 @@ import scala.compat.Platform object JWTAuth extends RestHelper with Loggable { serve { - //Handling get request for a "request token" + //Handling get request for an "authorization token" case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest) => { //Extract the jwtauth parameters from the header and test if the request is valid - var (httpCode, message, jwtAuthParameters) = validator("requestToken", "POST") + var (httpCode, message, jwtAuthParameters) = validator("authorizationToken", "POST") //Test if the request is valid if(httpCode==200) { @@ -60,43 +61,13 @@ object JWTAuth extends RestHelper with Loggable { val (token,secret) = generateTokenAndSecret(claims) //Save the token that we have generated - if(saveRequestToken(jwtAuthParameters,token, secret)) + if(saveAuthorizationToken(jwtAuthParameters,token, secret)) message="token="+token } val headers = ("Content-type" -> "application/x-www-form-urlencoded") :: Nil //return an HTTP response Full(InMemoryResponse(message.getBytes,headers,Nil,httpCode)) } - - case Req("my" :: "logins" :: "authorize" :: Nil,_, PostRequest) => authorize() - case Req("my" :: "logins" :: "authorize" :: Nil,_, GetRequest) => authorize() - - } - - def authorize() = - { - //Extract the jwtauth parameters from the header and test if the request is valid - var (httpCode, message, jwtAuthParameters) = validator("authorizationToken", "POST") - //Test if the request is valid - if(httpCode==200) - { - //Generate the token and secret/token - val claims = Map("app" -> "key") - val (token,secret) = generateTokenAndSecret(claims) - //Save the token that we have generated - if(saveAuthorizationToken(jwtAuthParameters,token, secret)) - //remove the request token so the application could not exchange it - //again to get an other access token - Token.find(By(Token.key,jwtAuthParameters.get("token").get)) match { - case Full(requestToken) => requestToken.delete_! - case _ => None - } - - message="token="+token+"&token_secret="+secret - } - val headers = ("Content-type" -> "application/x-www-form-urlencoded") :: Nil - //return an HTTP response - Full(InMemoryResponse(message.getBytes,headers,Nil,httpCode)) } //Check if the request (access token or request token) is valid and return a tuple @@ -105,7 +76,7 @@ object JWTAuth extends RestHelper with Loggable { def getAllParameters : Map[String,String]= { def toMapFromBasicAuth(paramsEncoded : String) = { - println("basic=" + paramsEncoded ) + //println("basic=" + paramsEncoded ) val params = new String(base64Decode(paramsEncoded.replaceAll("Basic ", ""))) params.toString.split(":") match { case Array(str1, str2) => Map("username" -> str1, "password" -> str2) @@ -166,11 +137,11 @@ object JWTAuth extends RestHelper with Loggable { //@return the missing parameters depending of the request type def missingjwtauthParameters(parameters : Map[String, String], requestType : String) : Set[String] = { - println(parameters.toString) + //println(parameters.toString) if(requestType == "requestToken") ("username" :: "password" :: List()).toSet diff parameters.keySet else if(requestType=="authorizationToken") - ("token" :: List()).toSet diff parameters.keySet + ("username" :: "password" :: List()).toSet diff parameters.keySet else if(requestType=="protectedResource") ("token" :: List()).toSet diff parameters.keySet else @@ -191,11 +162,11 @@ object JWTAuth extends RestHelper with Loggable { } //In the case jwtauth authorization token request, check if the token is still valid and the verifier is correct - else if(requestType=="authorizationToken" && !validRequestToken(parameters.get("token").get)) - { - message = "Invalid or expired request token: " + parameters.get("token").get - httpCode = 401 - } + //else if(requestType=="authorizationToken" && !validRequestToken(parameters.get("token").get)) + //{ + // message = "Invalid or expired request token: " + parameters.get("token").get + // httpCode = 401 + //} //In the case protected resource access request, check if the token is still valid else if ( requestType=="protectedResource" && @@ -231,40 +202,6 @@ object JWTAuth extends RestHelper with Loggable { (token_message, secret_message) } - private def saveRequestToken(jwtauthParameters : Map[String, String], tokenKey : String, tokenSecret : String) = - { - import code.model.{Token, TokenType} - - //val nonce = Nonce.create - //nonce.consumerkey(jwtauthParameters.get("consumer_key").get) - //nonce.timestamp(new Date(jwtauthParameters.get("timestamp").get.toLong)) - //nonce.value(jwtauthParameters.get("nonce").get) - //val nonceSaved = nonce.save() - - val token = Token.create - token.tokenType(TokenType.Request) - //if(! jwtauthParameters.get("consumer_key").get.isEmpty) - // Consumer.find(By(Consumer.key,jwtauthParameters.get("consumer_key").get)) match { - // case Full(consumer) => token.consumerId(consumer.id) - // case _ => None - //} - token.key(tokenKey) - token.secret(tokenSecret) - //if(! jwtauthParameters.get("callback").get.isEmpty) - // token.callbackURL(URLDecoder.decode(jwtauthParameters.get("callback").get,"UTF-8")) - //else - token.callbackURL("oob") - val currentTime = Platform.currentTime - val tokenDuration : Long = Helpers.minutes(30) - token.duration(tokenDuration) - token.expirationDate(new Date(currentTime+tokenDuration)) - token.insertDate(new Date(currentTime)) - val tokenSaved = token.save() - - // nonceSaved && tokenSaved - true && tokenSaved - } - private def saveAuthorizationToken(jwtauthParameters : Map[String, String], tokenKey : String, tokenSecret : String) = { import code.model.{Token, TokenType} @@ -282,10 +219,15 @@ object JWTAuth extends RestHelper with Loggable { // case Full(consumer) => token.consumerId(consumer.id) // case _ => None //} - Token.find(By(Token.key, jwtauthParameters.get("token").get)) match { - case Full(requestToken) => token.userForeignKey(requestToken.userForeignKey) - case _ => None - } + //Token.find(By(Token.key, jwtauthParameters.get("token").get)) match { + val userId = OBPUser.getUserId(jwtauthParameters.get("username").get, jwtauthParameters.get("password").get) + //Token.find(By(Token.key, tokenKey)) match { + // case Full(requestToken) => token.userForeignKey(requestToken.userForeignKey) + // case _ => None + //} + token.verifier("verifier") + token.consumerId(1) //TESTING ONLY + token.userForeignKey(userId) token.key(tokenKey) token.secret(tokenSecret) val currentTime = Platform.currentTime @@ -296,7 +238,7 @@ object JWTAuth extends RestHelper with Loggable { val tokenSaved = token.save() //nonceSaved && - tokenSaved + true && tokenSaved } def getUser : Box[User] = { @@ -306,8 +248,13 @@ object JWTAuth extends RestHelper with Loggable { } val (httpCode, message, jwtauthParameters) = validator("protectedResource", httpMethod) - if(httpCode== 200) getUser(httpCode, jwtauthParameters.get("token")) - else ParamFailure(message, Empty, Empty, APIFailure(message, httpCode)) + val user = getUser(200, jwtauthParameters.get("token")) + if (user != Empty ) { + val res = Full(user.get) + res + } else { + ParamFailure(message, Empty, Empty, APIFailure(message, httpCode)) + } } def getUser(httpCode : Int, tokenID : Box[String]) : Box[User] = From ad905e4e7913a0c5b155bfb59dd9fa537eb058e6 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 29 Jan 2016 23:17:56 +0100 Subject: [PATCH 322/702] Customer test improved --- src/test/scala/code/api/v1_4_0/CustomerTest.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index e602c9076..cd9914747 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -23,8 +23,9 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { dobOfDependents: List[Date], highestEducationAttained: String, employmentStatus: String, kycStatus: Boolean, lastOkDate: Date) extends Customer + val format = new java.text.SimpleDateFormat("dd/MM/yyyy") val mockCustomerFaceImage = MockFaceImage(new Date(1234000), "http://example.com/image1") - val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(new Date(1234000), new Date(1234000), new Date(1234000)), "Bachelor’s Degree", "Employed", true, new Date(1234000)) + val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(format.parse("30/03/2012"), format.parse("30/03/2012"), format.parse("30/03/2014")), "Bachelor’s Degree", "Employed", true, new Date(1234000)) object MockedCustomerProvider extends CustomerProvider { override def getCustomer(bankId: BankId, user: User): Box[Customer] = { @@ -112,7 +113,7 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { info.date_of_birth, info.relationship_status, info.dependants, - info.dob_of_dependants.sortBy(_.getTime), + info.dob_of_dependants, info.highest_education_attained, info.employment_status, info.kyc_status, From 777eab883b73c21db480aef38a50d5316c8d8cc1 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 29 Jan 2016 23:19:34 +0100 Subject: [PATCH 323/702] Code cleanup --- src/main/scala/code/api/OBPRestHelper.scala | 25 ++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index fe22f121a..958b81cfb 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -94,6 +94,7 @@ trait OBPRestHelper extends RestHelper with Loggable { } case Failure(msg, _, _) => { logger.info("jsonResponseBoxToJsonResponse case Failure API Failure: " + msg) + //throw(new Throwable("""INTENTIONAL EXCEPTION""")) errorJsonResponse(msg) } case _ => errorJsonResponse() @@ -118,7 +119,7 @@ trait OBPRestHelper extends RestHelper with Loggable { // Check if the content-type is text/json or application/json r.json_? match { case true => - //logger.info("failIfBadJSON says: Cool, content-type is json") + logger.info("failIfBadJSON says: Cool, content-type is json") r.json match { case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"Error: Invalid JSON: $msg")) case _ => h(r) @@ -135,7 +136,14 @@ trait OBPRestHelper extends RestHelper with Loggable { case Failure(msg, _, _) => errorJsonResponse(msg) case _ => errorJsonResponse("oauth error") } - } else fn(Empty) + } else { + val user = JWTAuth.getUser + if (user.isDefined) { + fn(user) + } else { + fn(Empty) + } + } } class RichStringList(list: List[String]) { @@ -152,8 +160,9 @@ trait OBPRestHelper extends RestHelper with Loggable { pf.isDefinedAt(req.withNewPath(req.path.drop(listLen))) } - def apply(req: Req): Box[User] => Box[JsonResponse] = + def apply(req: Req): Box[User] => Box[JsonResponse] = { pf.apply(req.withNewPath(req.path.drop(listLen))) + } } } @@ -179,9 +188,16 @@ trait OBPRestHelper extends RestHelper with Loggable { //if request is correct json //if request matches PartialFunction cases for each defined url //if request has correct oauth headers - failIfBadOauth { + val res = failIfBadOauth { failIfBadJSON(r, handler) } + if (res.code == 200) { + res + } + else { + res + } + //throw(new Throwable("""INTENTIONAL EXCEPTION""")) } def isDefinedAt(r : Req) = { //if the content-type is json and json parsing failed, simply accept call but then fail in apply() before @@ -215,5 +231,4 @@ trait OBPRestHelper extends RestHelper with Loggable { super.serve(obpHandler) } - } \ No newline at end of file From fbe37764676a802e94bd9f1adb88fc298ba9525c Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 29 Jan 2016 23:18:57 +0100 Subject: [PATCH 324/702] Task 2: Add these new resources to version 2.0.0 of the API in sandbox mode (Mapper) --- src/main/scala/bootstrap/liftweb/Boot.scala | 12 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 352 +++++++++++++++++- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 126 ++++++- .../scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 12 +- 4 files changed, 490 insertions(+), 12 deletions(-) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 922ce3037..ea9dadfc1 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -42,6 +42,11 @@ import code.atms.MappedAtm import code.branches.MappedBranch import code.crm.MappedCrmEvent import code.customer.{MappedCustomer, MappedCustomerMessage} +import code.kycdocuments.MappedKycDocument +import code.kycmedias.MappedKycMedia +import code.kycchecks.MappedKycCheck +import code.kycstatuses.MappedKycStatus +import code.socialmedia.MappedSocialMedia import code.management.{AccountsAPI, ImporterAPI} import code.metadata.comments.MappedComment import code.metadata.counterparties.{MappedCounterpartyMetadata, MappedCounterpartyWhereTag} @@ -401,5 +406,10 @@ object ToSchemify { MappedBranch, MappedAtm, MappedProduct, - MappedCrmEvent) + MappedCrmEvent, + MappedKycDocument, + MappedKycMedia, + MappedKycCheck, + MappedKycStatus, + MappedSocialMedia) } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index d127409ad..92f5ebfa2 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1,7 +1,8 @@ package code.api.v2_0_0 +import java.text.SimpleDateFormat + import code.api.util.APIUtil -import code.api.v1_2_1.APIMethods121 import net.liftweb.http.{JsonResponse, Req} import net.liftweb.json.Extraction import net.liftweb.common._ @@ -10,20 +11,20 @@ import net.liftweb.json.JsonAST.JValue import APIUtil._ import net.liftweb.util.Helpers._ import net.liftweb.http.rest.RestHelper -import java.net.URL -import net.liftweb.util.Props -import code.bankconnectors._ -import code.bankconnectors.OBPOffset -import code.bankconnectors.OBPFromDate -import code.bankconnectors.OBPToDate -import code.model.ViewCreationJSON import net.liftweb.common.Full -import code.model.ViewUpdateData +import code.kycdocuments.KycDocuments +import code.kycmedias.KycMedias +import code.kycstatuses.KycStatuses +import code.kycchecks.KycChecks +import code.socialmedia.{SocialMedias, SocialMedia} import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer // Makes JValue assignment to Nil work import net.liftweb.json.JsonDSL._ +import code.customer.{Customer} +import code.util.Helper._ +import net.liftweb.http.js.JE.JsRaw trait APIMethods200 { @@ -58,6 +59,10 @@ trait APIMethods200 { val emptyObjectJson : JValue = Nil val apiVersion : String = "2_0_0" + val exampleDateString : String ="22/08/2013" + val simpleDateFormat : SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") + val exampleDate = simpleDateFormat.parse(exampleDateString) + resourceDocs += ResourceDoc( allAccountsAllBanks, apiVersion, @@ -240,10 +245,339 @@ trait APIMethods200 { } } + resourceDocs += ResourceDoc( + getKycDocuments, + apiVersion, + "getKycDocuments", + "GET", + "/customers/CUSTOMER_NUMBER/kyc_document", + "Get documents for the logged in customer", + """Messages sent to the currently authenticated user. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val getKycDocuments : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "kyc_document" :: Nil JsonGet _ => { + user => { + for { + u <- user ?~! "User must be logged in to retrieve customer messages" + cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} + } yield { + val kycDocuments = KycDocuments.kycDocumentProvider.vend.getKycDocuments(cNumber) + val json = JSONFactory.createKycDocumentsJSON(kycDocuments) + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + + + resourceDocs += ResourceDoc( + getKycMedias, + apiVersion, + "getKycMedias", + "GET", + "/customers/CUSTOMER_NUMBER/kyc_media", + "Get medias for the logged in customer", + """Messages sent to the currently authenticated user. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val getKycMedias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "kyc_media" :: Nil JsonGet _ => { + user => { + for { + u <- user ?~! "User must be logged in to retrieve customer messages" + cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} + } yield { + val kycMedias = KycMedias.kycMediaProvider.vend.getKycMedias(cNumber) + val json = JSONFactory.createKycMediasJSON(kycMedias) + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + + resourceDocs += ResourceDoc( + getKycCheck, + apiVersion, + "getKycCheck", + "GET", + "/customers/CUSTOMER_NUMBER/kyc_check", + "Get checks for the logged in customer", + """Messages sent to the currently authenticated user. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val getKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "kyc_check" :: Nil JsonGet _ => { + user => { + for { + u <- user ?~! "User must be logged in to retrieve customer messages" + cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} + } yield { + val kycChecks = KycChecks.kycCheckProvider.vend.getKycChecks(cNumber) + val json = JSONFactory.createKycChecksJSON(kycChecks) + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + resourceDocs += ResourceDoc( + getKycStatuses, + apiVersion, + "getKycStatuses", + "GET", + "/customers/CUSTOMER_NUMBER/kyc_status", + "Get statuses for the logged in customer", + """Messages sent to the currently authenticated user. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val getKycStatuses : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "kyc_status" :: Nil JsonGet _ => { + user => { + for { + u <- user ?~! "User must be logged in to retrieve customer messages" + cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} + } yield { + val kycStatuses = KycStatuses.kycStatusProvider.vend.getKycStatuses(cNumber) + val json = JSONFactory.createKycStatusesJSON(kycStatuses) + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + + resourceDocs += ResourceDoc( + getSocialMedia, + apiVersion, + "getSocialMedia", + "GET", + "/customers/CUSTOMER_NUMBER/social_media", + "Get social medias for the logged in customer", + """Messages sent to the currently authenticated user. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil) + + lazy val getSocialMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "social_media" :: Nil JsonGet _ => { + user => { + for { + u <- user ?~! "User must be logged in to retrieve customer messages" + cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} + } yield { + val kycSocialMedias = SocialMedias.socialMediaProvider.vend.getSocialMedias(cNumber) + val json = JSONFactory.createSocialMediasJSON(kycSocialMedias) + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + resourceDocs += ResourceDoc( + addKycDocument, + apiVersion, + "addKycDocument", + "POST", + "/banks/BANK_ID/customer/kyc_document", + "Add a kyc_document for the customer specified by CUSTOMER_NUMBER", + "", + Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), + emptyObjectJson, + emptyObjectJson :: Nil + ) + + lazy val addKycDocument : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customer" :: "kyc_document" :: Nil JsonPost json -> _ => { + user => { + for { + u <- user ?~! "User must be logged in to post Document" + postedData <- tryo{json.extract[KycDocumentJSON]} ?~! "Incorrect json format" + bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} + customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" + kycDocumentCreated <- booleanToBox( + KycDocuments.kycDocumentProvider.vend.addKycDocuments( + postedData.id, + postedData.customer_number, + postedData.`type`, + postedData.number, + postedData.issue_date, + postedData.issue_place, + postedData.expiry_date), + "Server error: could not add message") + } yield { + successJsonResponse(JsRaw("{}"), 201) + } + } + } + } + + resourceDocs += ResourceDoc( + addKycMedia, + apiVersion, + "addKycMedia", + "POST", + "/banks/BANK_ID/customer/kyc_media", + "Add a kyc_media for the customer specified by CUSTOMER_NUMBER", + "", + Extraction.decompose(KycMediaJSON("73hyfgayt6ywerwerasd", "1239879", "image", "http://www.example.com/id-docs/123/image.png", exampleDate, "wuwjfuha234678", "98FRd987auhf87jab")), + emptyObjectJson, + emptyObjectJson :: Nil + ) + + lazy val addKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customer" :: "kyc_media" :: Nil JsonPost json -> _ => { + user => { + for { + u <- user ?~! "User must be logged in to post Document" + postedData <- tryo{json.extract[KycMediaJSON]} ?~! "Incorrect json format" + bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} + customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" + kycDocumentCreated <- booleanToBox( + KycMedias.kycMediaProvider.vend.addKycMedias( + postedData.id, + postedData.customer_number, + postedData.`type`, + postedData.url, + postedData.date, + postedData.relates_to_kyc_document_id, + postedData.relates_to_kyc_check_id), + "Server error: could not add message") + } yield { + successJsonResponse(JsRaw("{}"), 201) + } + } + } + } + + resourceDocs += ResourceDoc( + addKycCheck, + apiVersion, + "addKycCheck", + "POST", + "/banks/BANK_ID/customer/kyc_check", + "Add a kyc_check for the customer specified by CUSTOMER_NUMBER", + "", + Extraction.decompose(KycCheckJSON("98FRd987auhf87jab", "1239879", exampleDate, "online_meeting", "67876", "Simon Redfern", true, "")), + emptyObjectJson, + emptyObjectJson :: Nil + ) + + lazy val addKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customer" :: "kyc_check" :: Nil JsonPost json -> _ => { + user => { + for { + u <- user ?~! "User must be logged in to post Document" + postedData <- tryo{json.extract[KycCheckJSON]} ?~! "Incorrect json format" + bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} + customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" + kycCheckCreated <- booleanToBox( + KycChecks.kycCheckProvider.vend.addKycChecks( + postedData.id, + postedData.customer_number, + postedData.date, + postedData.how, + postedData.staff_user_id, + postedData.staff_name, + postedData.satisfied, + postedData.comments), + "Server error: could not add message") + } yield { + successJsonResponse(JsRaw("{}"), 201) + } + } + } + } + + resourceDocs += ResourceDoc( + addKycStatus, + apiVersion, + "addKycStatus", + "POST", + "/banks/BANK_ID/customer/kyc_status", + "Add a kyc_status for the customer specified by CUSTOMER_NUMBER", + "", + Extraction.decompose(KycStatusJSON("8762893876", true, exampleDate)), + emptyObjectJson, + emptyObjectJson :: Nil + ) + + lazy val addKycStatus : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customer" :: "kyc_status" :: Nil JsonPost json -> _ => { + user => { + for { + u <- user ?~! "User must be logged in to post Document" + postedData <- tryo{json.extract[KycStatusJSON]} ?~! "Incorrect json format" + bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} + customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" + kycStatusCreated <- booleanToBox( + KycStatuses.kycStatusProvider.vend.addKycStatus( + postedData.customer_number, + postedData.ok, + postedData.date), + "Server error: could not add message") + } yield { + successJsonResponse(JsRaw("{}"), 201) + } + } + } + } + + resourceDocs += ResourceDoc( + addSocialMedia, + apiVersion, + "addSocialMedia", + "POST", + "/banks/BANK_ID/customer/social_media", + "Add a social_media for the customer specified by CUSTOMER_NUMBER", + "", + Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), + emptyObjectJson, + emptyObjectJson :: Nil + ) + + lazy val addSocialMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customer" :: "social_media" :: Nil JsonPost json -> _ => { + user => { + for { + u <- user ?~! "User must be logged in to post Document" + postedData <- tryo{json.extract[SocialMediaJSON]} ?~! "Incorrect json format" + bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} + customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" + kycSocialMediaCreated <- booleanToBox( + SocialMedias.socialMediaProvider.vend.addSocialMedias( + postedData.customer_number, + postedData.`type`, + postedData.handle, + postedData.date_added, + postedData.date_activated), + "Server error: could not add message") + } yield { + successJsonResponse(JsRaw("{}"), 201) + } + } + } + } + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index ec12b85c6..995afbd56 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -32,6 +32,11 @@ Berlin 13359, Germany package code.api.v2_0_0 import java.util.Date +import code.kycdocuments.KycDocument +import code.kycmedias.KycMedia +import code.kycstatuses.KycStatus +import code.kycchecks.KycCheck +import code.socialmedia.SocialMedia import net.liftweb.common.{Box, Full} import code.model._ @@ -58,6 +63,55 @@ case class AccountBasicJSON( bank_id : String ) +case class KycDocumentJSON( + id: String, + customer_number: String, + `type`: String, + number: String, + issue_date: Date, + issue_place: String, + expiry_date: Date +) +case class KycDocumentsJSON(documents: List[KycDocumentJSON]) + +case class KycMediaJSON( + id: String, + customer_number: String, + `type`: String, + url: String, + date: Date, + relates_to_kyc_document_id: String, + relates_to_kyc_check_id: String +) +case class KycMediasJSON(medias: List[KycMediaJSON]) + +case class KycCheckJSON( + id: String, + customer_number: String, + date: Date, + how: String, + staff_user_id: String, + staff_name: String, + satisfied: Boolean, + comments: String +) +case class KycChecksJSON(checks: List[KycCheckJSON]) + +case class KycStatusJSON( + customer_number: String, + ok: Boolean, + date: Date +) +case class KycStatusesJSON(statuses: List[KycStatusJSON]) + +case class SocialMediaJSON( + customer_number: String, + `type`: String, + handle: String, + date_added: Date, + date_activated: Date +) +case class SocialMediasJSON(checks: List[SocialMediaJSON]) object JSONFactory{ @@ -89,7 +143,77 @@ object JSONFactory{ account.bankId.value ) } - + + def createKycDocumentJSON(kycDocument : KycDocument) : KycDocumentJSON = { + new KycDocumentJSON( + id = kycDocument.idKycDocument, + customer_number = kycDocument.customerNumber, + `type` = kycDocument.`type`, + number = kycDocument.number, + issue_date = kycDocument.issueDate, + issue_place = kycDocument.issuePlace, + expiry_date = kycDocument.expiryDate + ) + } + + def createKycDocumentsJSON(messages : List[KycDocument]) : KycDocumentsJSON = { + KycDocumentsJSON(messages.map(createKycDocumentJSON)) + } + + def createKycMediaJSON(kycMedia : KycMedia) : KycMediaJSON = { + new KycMediaJSON( + id = kycMedia.idKycMedia, + customer_number = kycMedia.customerNumber, + `type` = kycMedia.`type`, + url = kycMedia.url, + date = kycMedia.date, + relates_to_kyc_document_id = kycMedia.relatesToKycDocumentId, + relates_to_kyc_check_id = kycMedia.relatesToKycCheckId + ) + } + def createKycMediasJSON(messages : List[KycMedia]) : KycMediasJSON = { + KycMediasJSON(messages.map(createKycMediaJSON)) + } + + def createKycCheckJSON(kycCheck : KycCheck) : KycCheckJSON = { + new KycCheckJSON( + id = kycCheck.idKycCheck, + customer_number = kycCheck.customerNumber, + date = kycCheck.date, + how = kycCheck.how, + staff_user_id = kycCheck.staffUserId, + staff_name = kycCheck.staffName, + satisfied = kycCheck.satisfied, + comments = kycCheck.comments + ) + } + def createKycChecksJSON(messages : List[KycCheck]) : KycChecksJSON = { + KycChecksJSON(messages.map(createKycCheckJSON)) + } + + def createKycStatusJSON(kycStatus : KycStatus) : KycStatusJSON = { + new KycStatusJSON( + customer_number = kycStatus.customerNumber, + ok = kycStatus.ok, + date = kycStatus.date + ) + } + def createKycStatusesJSON(messages : List[KycStatus]) : KycStatusesJSON = { + KycStatusesJSON(messages.map(createKycStatusJSON)) + } + + def createSocialMediaJSON(socialMedia : SocialMedia) : SocialMediaJSON = { + new SocialMediaJSON( + customer_number = socialMedia.customerNumber, + `type` = socialMedia.`type`, + handle = socialMedia.handle, + date_added = socialMedia.dateAdded, + date_activated = socialMedia.dateActivated + ) + } + def createSocialMediasJSON(messages : List[SocialMedia]) : SocialMediasJSON = { + SocialMediasJSON(messages.map(createSocialMediaJSON)) + } // From 1.2.1 def stringOrNull(text : String) = diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala index a32ee54c8..78b7ac69e 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -144,7 +144,17 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.publicAccountsAllBanks, Implementations2_0_0.allAccountsAtOneBank, Implementations2_0_0.privateAccountsAtOneBank, - Implementations2_0_0.publicAccountsAtOneBank + Implementations2_0_0.publicAccountsAtOneBank, + Implementations2_0_0.getKycDocuments, + Implementations2_0_0.getKycMedias, + Implementations2_0_0.getKycStatuses, + Implementations2_0_0.getKycCheck, + Implementations2_0_0.getSocialMedia, + Implementations2_0_0.addKycDocument, + Implementations2_0_0.addKycMedia, + Implementations2_0_0.addKycStatus, + Implementations2_0_0.addKycCheck, + Implementations2_0_0.addSocialMedia ) routes.foreach(route => { From bb23e32dfc4349f04ddc4ff97ec57474f5dda2c1 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 29 Jan 2016 23:24:11 +0100 Subject: [PATCH 325/702] Task 2: Add these new resources to version 2.0.0 of the API in sandbox mode (Mapper) --- src/main/scala/code/kyccheck/KycCheck.scala | 32 ++++++++++ .../kyccheck/MappedKycChecksProvider.scala | 64 +++++++++++++++++++ .../code/kycdocuments/KycDocuments.scala | 31 +++++++++ .../MappedKycDocumentsProvider.scala | 60 +++++++++++++++++ src/main/scala/code/kycmedia/KycMedia.scala | 33 ++++++++++ .../kycmedia/MappedKycMediasProvider.scala | 60 +++++++++++++++++ src/main/scala/code/kycstatus/KycStatus.scala | 29 +++++++++ .../kycstatus/MappedKycStatusesProvider.scala | 50 +++++++++++++++ .../MappedSocialMediasProvider.scala | 52 +++++++++++++++ .../scala/code/socialmedia/SocialMedia.scala | 29 +++++++++ 10 files changed, 440 insertions(+) create mode 100644 src/main/scala/code/kyccheck/KycCheck.scala create mode 100644 src/main/scala/code/kyccheck/MappedKycChecksProvider.scala create mode 100644 src/main/scala/code/kycdocuments/KycDocuments.scala create mode 100644 src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala create mode 100644 src/main/scala/code/kycmedia/KycMedia.scala create mode 100644 src/main/scala/code/kycmedia/MappedKycMediasProvider.scala create mode 100644 src/main/scala/code/kycstatus/KycStatus.scala create mode 100644 src/main/scala/code/kycstatus/MappedKycStatusesProvider.scala create mode 100644 src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala create mode 100644 src/main/scala/code/socialmedia/SocialMedia.scala diff --git a/src/main/scala/code/kyccheck/KycCheck.scala b/src/main/scala/code/kyccheck/KycCheck.scala new file mode 100644 index 000000000..52490c7a9 --- /dev/null +++ b/src/main/scala/code/kyccheck/KycCheck.scala @@ -0,0 +1,32 @@ +package code.kycchecks + +import java.util.Date +import net.liftweb.util.SimpleInjector + + +object KycChecks extends SimpleInjector { + + val kycCheckProvider = new Inject(buildOne _) {} + + def buildOne: KycCheckProvider = MappedKycChecksProvider + +} + +trait KycCheckProvider { + + def getKycChecks(customerNumber: String) : List[KycCheck] + + def addKycChecks(id: String, customerNumber: String, date: Date, how: String, staffUserId: String, mStaffName: String, mSatisfied: Boolean, comments: String) : Boolean + +} + +trait KycCheck { + def idKycCheck : String + def customerNumber : String + def date : Date + def how : String + def staffUserId : String + def staffName : String + def satisfied: Boolean + def comments : String +} \ No newline at end of file diff --git a/src/main/scala/code/kyccheck/MappedKycChecksProvider.scala b/src/main/scala/code/kyccheck/MappedKycChecksProvider.scala new file mode 100644 index 000000000..b4eb8c1a2 --- /dev/null +++ b/src/main/scala/code/kyccheck/MappedKycChecksProvider.scala @@ -0,0 +1,64 @@ +package code.kycchecks + +import java.util.Date + +import code.model.{BankId, User} +import code.model.dataAccess.APIUser +import code.util.{DefaultStringField} +import net.liftweb.mapper._ + +object MappedKycChecksProvider extends KycCheckProvider { + + override def getKycChecks(customerNumber: String): List[MappedKycCheck] = { + MappedKycCheck.findAll( + By(MappedKycCheck.mCustomerNumber, customerNumber), + OrderBy(MappedKycCheck.updatedAt, Descending)) + } + + + override def addKycChecks(id: String, customerNumber: String, date: Date, how: String, staffUserId: String, mStaffName: String, mSatisfied: Boolean, comments: String): Boolean = { + MappedKycCheck.create + .mId(id) + .mCustomerNumber(customerNumber) + .mDate(date) + .mHow(how) + .mStaffUserId(staffUserId) + .mStaffName(mStaffName) + .mSatisfied(mSatisfied) + .mComments(comments) + .save() + } +} + +class MappedKycCheck extends KycCheck +with LongKeyedMapper[MappedKycCheck] with IdPK with CreatedUpdated { + + def getSingleton = MappedKycCheck + + object user extends MappedLongForeignKey(this, APIUser) + object bank extends DefaultStringField(this) + + object mId extends DefaultStringField(this) + object mCustomerNumber extends DefaultStringField(this) + object mDate extends MappedDateTime(this) + object mHow extends DefaultStringField(this) + object mStaffUserId extends DefaultStringField(this) + object mStaffName extends DefaultStringField(this) + object mSatisfied extends MappedBoolean(this) + object mComments extends DefaultStringField(this) + + + + override def idKycCheck: String = mId.get + override def customerNumber: String = mCustomerNumber.get + override def date: Date = mDate.get + override def how: String = mHow.get + override def staffUserId: String = mStaffUserId.get + override def staffName: String = mStaffName.get + override def satisfied: Boolean = mSatisfied.get + override def comments: String = mComments.get +} + +object MappedKycCheck extends MappedKycCheck with LongKeyedMetaMapper[MappedKycCheck] { + override def dbIndexes = UniqueIndex(mId) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/kycdocuments/KycDocuments.scala b/src/main/scala/code/kycdocuments/KycDocuments.scala new file mode 100644 index 000000000..f61a0ed53 --- /dev/null +++ b/src/main/scala/code/kycdocuments/KycDocuments.scala @@ -0,0 +1,31 @@ +package code.kycdocuments + +import java.util.Date +import net.liftweb.util.SimpleInjector + + +object KycDocuments extends SimpleInjector { + + val kycDocumentProvider = new Inject(buildOne _) {} + + def buildOne: KycDocumentProvider = MappedKycDocumentsProvider + +} + +trait KycDocumentProvider { + + def getKycDocuments(customerNumber: String) : List[KycDocument] + + def addKycDocuments(id: String, customerNumber: String, `type`: String, number: String, issueDate: Date, issuePlace: String, expiryDate: Date) : Boolean + +} + +trait KycDocument { + def idKycDocument : String + def customerNumber : String + def `type` : String + def number : String + def issueDate : Date + def issuePlace : String + def expiryDate : Date +} \ No newline at end of file diff --git a/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala b/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala new file mode 100644 index 000000000..325e58dac --- /dev/null +++ b/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala @@ -0,0 +1,60 @@ +package code.kycdocuments + +import java.util.Date + +import code.model.{BankId, User} +import code.model.dataAccess.APIUser +import code.util.{DefaultStringField} +import net.liftweb.mapper._ + +object MappedKycDocumentsProvider extends KycDocumentProvider { + + override def getKycDocuments(customerNumber: String): List[MappedKycDocument] = { + MappedKycDocument.findAll( + By(MappedKycDocument.mCustomerNumber, customerNumber), + OrderBy(MappedKycDocument.updatedAt, Descending)) + } + + + override def addKycDocuments(id: String, customerNumber: String, `type`: String, number: String, issueDate: Date, issuePlace: String, expiryDate: Date): Boolean = { + MappedKycDocument.create + .mId(id) + .mCustomerNumber(customerNumber) + .mType(`type`) + .mNumber(number) + .mIssueDate(issueDate) + .mIssuePlace(issuePlace) + .mExpiryDate(expiryDate) + .save() + } +} + +class MappedKycDocument extends KycDocument +with LongKeyedMapper[MappedKycDocument] with IdPK with CreatedUpdated { + + def getSingleton = MappedKycDocument + + object user extends MappedLongForeignKey(this, APIUser) + object bank extends DefaultStringField(this) + + object mId extends DefaultStringField(this) + object mCustomerNumber extends DefaultStringField(this) + object mType extends DefaultStringField(this) + object mNumber extends DefaultStringField(this) + object mIssueDate extends MappedDateTime(this) + object mIssuePlace extends DefaultStringField(this) + object mExpiryDate extends MappedDateTime(this) + + + override def idKycDocument: String = mId.get + override def customerNumber: String = mCustomerNumber.get + override def `type`: String = mType.get + override def number: String = mNumber.get + override def issueDate: Date = mIssueDate.get + override def issuePlace: String = mIssuePlace.get + override def expiryDate: Date = mExpiryDate.get +} + +object MappedKycDocument extends MappedKycDocument with LongKeyedMetaMapper[MappedKycDocument] { + override def dbIndexes = UniqueIndex(mId) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/kycmedia/KycMedia.scala b/src/main/scala/code/kycmedia/KycMedia.scala new file mode 100644 index 000000000..98143adfd --- /dev/null +++ b/src/main/scala/code/kycmedia/KycMedia.scala @@ -0,0 +1,33 @@ +package code.kycmedias + +import java.util.Date + +import code.model.{BankId, User} +import net.liftweb.util.SimpleInjector + + +object KycMedias extends SimpleInjector { + + val kycMediaProvider = new Inject(buildOne _) {} + + def buildOne: KycMediaProvider = MappedKycMediasProvider + +} + +trait KycMediaProvider { + + def getKycMedias(customerNumber: String) : List[KycMedia] + + def addKycMedias(id: String, customerNumber: String, `type`: String, url: String, date: Date, relatesToKycDocumentId: String, relatesToKycCheckId: String) : Boolean + +} + +trait KycMedia { + def idKycMedia : String + def customerNumber : String + def `type` : String + def url : String + def date : Date + def relatesToKycDocumentId : String + def relatesToKycCheckId : String +} \ No newline at end of file diff --git a/src/main/scala/code/kycmedia/MappedKycMediasProvider.scala b/src/main/scala/code/kycmedia/MappedKycMediasProvider.scala new file mode 100644 index 000000000..5e249ca8a --- /dev/null +++ b/src/main/scala/code/kycmedia/MappedKycMediasProvider.scala @@ -0,0 +1,60 @@ +package code.kycmedias + +import java.util.Date + +import code.model.{BankId, User} +import code.model.dataAccess.APIUser +import code.util.{DefaultStringField} +import net.liftweb.mapper._ + +object MappedKycMediasProvider extends KycMediaProvider { + + override def getKycMedias(customerNumber: String): List[MappedKycMedia] = { + MappedKycMedia.findAll( + By(MappedKycMedia.mCustomerNumber, customerNumber), + OrderBy(MappedKycMedia.updatedAt, Descending)) + } + + + override def addKycMedias(id: String, customerNumber: String, `type`: String, url: String, date: Date, relatesToKycDocumentId: String, relatesToKycCheckId: String): Boolean = { + MappedKycMedia.create + .mId(id) + .mCustomerNumber(customerNumber) + .mType(`type`) + .mUrl(url) + .mDate(date) + .mRelatesToKycDocumentId(relatesToKycDocumentId) + .mRelatesToKycCheckId(relatesToKycCheckId) + .save() + } +} + +class MappedKycMedia extends KycMedia +with LongKeyedMapper[MappedKycMedia] with IdPK with CreatedUpdated { + + def getSingleton = MappedKycMedia + + object user extends MappedLongForeignKey(this, APIUser) + object bank extends DefaultStringField(this) + + object mId extends DefaultStringField(this) + object mCustomerNumber extends DefaultStringField(this) + object mType extends DefaultStringField(this) + object mUrl extends DefaultStringField(this) + object mDate extends MappedDateTime(this) + object mRelatesToKycDocumentId extends DefaultStringField(this) + object mRelatesToKycCheckId extends DefaultStringField(this) + + + override def idKycMedia: String = mId.get + override def customerNumber: String = mCustomerNumber.get + override def `type`: String = mType.get + override def url: String = mUrl.get + override def date: Date = mDate.get + override def relatesToKycDocumentId: String = mRelatesToKycDocumentId.get + override def relatesToKycCheckId: String = mRelatesToKycCheckId.get +} + +object MappedKycMedia extends MappedKycMedia with LongKeyedMetaMapper[MappedKycMedia] { + override def dbIndexes = UniqueIndex(mId) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/kycstatus/KycStatus.scala b/src/main/scala/code/kycstatus/KycStatus.scala new file mode 100644 index 000000000..9c6b1c344 --- /dev/null +++ b/src/main/scala/code/kycstatus/KycStatus.scala @@ -0,0 +1,29 @@ +package code.kycstatuses + +import java.util.Date + +import code.model.{BankId, User} +import net.liftweb.util.SimpleInjector + + +object KycStatuses extends SimpleInjector { + + val kycStatusProvider = new Inject(buildOne _) {} + + def buildOne: KycStatusProvider = MappedKycStatusesProvider + +} + +trait KycStatusProvider { + + def getKycStatuses(customerNumber: String) : List[KycStatus] + + def addKycStatus(customerNumber: String, ok: Boolean, date: Date) : Boolean + +} + +trait KycStatus { + def customerNumber : String + def ok : Boolean + def date : Date +} \ No newline at end of file diff --git a/src/main/scala/code/kycstatus/MappedKycStatusesProvider.scala b/src/main/scala/code/kycstatus/MappedKycStatusesProvider.scala new file mode 100644 index 000000000..90959f8ef --- /dev/null +++ b/src/main/scala/code/kycstatus/MappedKycStatusesProvider.scala @@ -0,0 +1,50 @@ +package code.kycstatuses + +import java.util.Date + +import code.model.{BankId, User} +import code.model.dataAccess.APIUser +import code.util.{DefaultStringField} +import net.liftweb.mapper._ + +object MappedKycStatusesProvider extends KycStatusProvider { + + override def getKycStatuses(customerNumber: String): List[MappedKycStatus] = { + MappedKycStatus.findAll( + By(MappedKycStatus.mCustomerNumber, customerNumber), + OrderBy(MappedKycStatus.updatedAt, Descending)) + } + + + override def addKycStatus(customerNumber: String, ok: Boolean, date: Date): Boolean = { + MappedKycStatus.create + .mCustomerNumber(customerNumber) + .mOk(ok) + .mDate(date) + .save() + } +} + +class MappedKycStatus extends KycStatus +with LongKeyedMapper[MappedKycStatus] with IdPK with CreatedUpdated { + + def getSingleton = MappedKycStatus + + object user extends MappedLongForeignKey(this, APIUser) + object bank extends DefaultStringField(this) + + object mCustomerNumber extends DefaultStringField(this) + object mOk extends MappedBoolean(this) + object mDate extends MappedDateTime(this) + + + + override def customerNumber: String = mCustomerNumber.get + override def ok: Boolean = mOk.get + override def date: Date = mDate.get + +} + +object MappedKycStatus extends MappedKycStatus with LongKeyedMetaMapper[MappedKycStatus] { + override def dbIndexes = UniqueIndex(mCustomerNumber) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala b/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala new file mode 100644 index 000000000..524713368 --- /dev/null +++ b/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala @@ -0,0 +1,52 @@ +package code.socialmedia + +import java.util.Date +import code.model.dataAccess.APIUser +import code.util.{DefaultStringField} +import net.liftweb.mapper._ + +object MappedSocialMediasProvider extends SocialMediaProvider { + + override def getSocialMedias(customerNumber: String): List[MappedSocialMedia] = { + MappedSocialMedia.findAll( + By(MappedSocialMedia.mCustomerNumber, customerNumber), + OrderBy(MappedSocialMedia.updatedAt, Descending)) + } + + + override def addSocialMedias(customerNumber: String, `type`: String, handle: String, dateAdded: Date, dateActivated: Date): Boolean = { + MappedSocialMedia.create + .mCustomerNumber(customerNumber) + .mType(`type`) + .mHandle(handle) + .mDateAdded(dateAdded) + .mDateActivated(dateActivated) + .save() + } +} + +class MappedSocialMedia extends SocialMedia +with LongKeyedMapper[MappedSocialMedia] with IdPK with CreatedUpdated { + + def getSingleton = MappedSocialMedia + + object user extends MappedLongForeignKey(this, APIUser) + object bank extends DefaultStringField(this) + + object mCustomerNumber extends DefaultStringField(this) + object mType extends DefaultStringField(this) + object mHandle extends DefaultStringField(this) + object mDateAdded extends MappedDateTime(this) + object mDateActivated extends MappedDateTime(this) + + + override def customerNumber: String = mCustomerNumber.get + override def `type`: String = mType.get + override def handle: String = mHandle.get + override def dateAdded: Date = mDateAdded.get + override def dateActivated: Date = mDateActivated.get +} + +object MappedSocialMedia extends MappedSocialMedia with LongKeyedMetaMapper[MappedSocialMedia] { + override def dbIndexes = UniqueIndex(mCustomerNumber) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/socialmedia/SocialMedia.scala b/src/main/scala/code/socialmedia/SocialMedia.scala new file mode 100644 index 000000000..3c9f18902 --- /dev/null +++ b/src/main/scala/code/socialmedia/SocialMedia.scala @@ -0,0 +1,29 @@ +package code.socialmedia + +import java.util.Date +import net.liftweb.util.SimpleInjector + + +object SocialMedias extends SimpleInjector { + + val socialMediaProvider = new Inject(buildOne _) {} + + def buildOne: SocialMediaProvider = MappedSocialMediasProvider + +} + +trait SocialMediaProvider { + + def getSocialMedias(customerNumber: String) : List[SocialMedia] + + def addSocialMedias(customerNumber: String, `type`: String, handle: String, dateAdded: Date, dateActivated: Date) : Boolean + +} + +trait SocialMedia { + def customerNumber : String + def `type` : String + def handle : String + def dateAdded : Date + def dateActivated : Date +} \ No newline at end of file From d6d0f1feb21b42929d190fdc2eb640b1b7faa69e Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 29 Jan 2016 23:29:02 +0100 Subject: [PATCH 326/702] Code cleanup --- src/main/scala/code/api/OBPRestHelper.scala | 35 ++++++--------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 958b81cfb..e2176f9d3 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -28,7 +28,7 @@ Berlin 13359, Germany Everett Sochowski : everett AT tesobe DOT com Ayoub Benali: ayoub AT tesobe DOT com - */ + */ package code.api @@ -61,7 +61,7 @@ object APIFailure { //that all stable versions retain the same behavior case class UserNotFound(providerId : String, userId: String) extends APIFailure { val responseCode = 400 //TODO: better as 404? -> would break some backwards compatibility (or at least the tests!) - + //to reiterate the comment about preserving backwards compatibility: //consider the case that an app may be parsing this string to decide what message to show their users //e.g. when granting view permissions, an app may not give their users a choice of provider and only @@ -94,7 +94,6 @@ trait OBPRestHelper extends RestHelper with Loggable { } case Failure(msg, _, _) => { logger.info("jsonResponseBoxToJsonResponse case Failure API Failure: " + msg) - //throw(new Throwable("""INTENTIONAL EXCEPTION""")) errorJsonResponse(msg) } case _ => errorJsonResponse() @@ -119,7 +118,7 @@ trait OBPRestHelper extends RestHelper with Loggable { // Check if the content-type is text/json or application/json r.json_? match { case true => - logger.info("failIfBadJSON says: Cool, content-type is json") + //logger.info("failIfBadJSON says: Cool, content-type is json") r.json match { case Failure(msg, _, _) => (x: Box[User]) => Full(errorJsonResponse(s"Error: Invalid JSON: $msg")) case _ => h(r) @@ -136,23 +135,16 @@ trait OBPRestHelper extends RestHelper with Loggable { case Failure(msg, _, _) => errorJsonResponse(msg) case _ => errorJsonResponse("oauth error") } - } else { - val user = JWTAuth.getUser - if (user.isDefined) { - fn(user) - } else { - fn(Empty) - } - } + } else fn(Empty) } class RichStringList(list: List[String]) { val listLen = list.length /** - * Normally we would use ListServeMagic's prefix function, but it works with PartialFunction[Req, () => Box[LiftResponse]] - * instead of the PartialFunction[Req, Box[User] => Box[JsonResponse]] that we need. This function does the same thing, really. - */ + * Normally we would use ListServeMagic's prefix function, but it works with PartialFunction[Req, () => Box[LiftResponse]] + * instead of the PartialFunction[Req, Box[User] => Box[JsonResponse]] that we need. This function does the same thing, really. + */ def oPrefix(pf: PartialFunction[Req, Box[User] => Box[JsonResponse]]): PartialFunction[Req, Box[User] => Box[JsonResponse]] = new PartialFunction[Req, Box[User] => Box[JsonResponse]] { def isDefinedAt(req: Req): Boolean = @@ -160,9 +152,8 @@ trait OBPRestHelper extends RestHelper with Loggable { pf.isDefinedAt(req.withNewPath(req.path.drop(listLen))) } - def apply(req: Req): Box[User] => Box[JsonResponse] = { + def apply(req: Req): Box[User] => Box[JsonResponse] = pf.apply(req.withNewPath(req.path.drop(listLen))) - } } } @@ -188,16 +179,9 @@ trait OBPRestHelper extends RestHelper with Loggable { //if request is correct json //if request matches PartialFunction cases for each defined url //if request has correct oauth headers - val res = failIfBadOauth { + failIfBadOauth { failIfBadJSON(r, handler) } - if (res.code == 200) { - res - } - else { - res - } - //throw(new Throwable("""INTENTIONAL EXCEPTION""")) } def isDefinedAt(r : Req) = { //if the content-type is json and json parsing failed, simply accept call but then fail in apply() before @@ -231,4 +215,5 @@ trait OBPRestHelper extends RestHelper with Loggable { super.serve(obpHandler) } + } \ No newline at end of file From 7ebb8f7f6c4158bea46f3fc605948405b8a27e7d Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 30 Jan 2016 00:24:29 +0000 Subject: [PATCH 327/702] Tweaking urls and variable names for KYC calls --- .../scala/code/api/v2_0_0/APIMethods200.scala | 115 ++++++++++-------- .../scala/code/api/v2_0_0/OBPAPI2.0.0.scala | 8 +- .../MappedSocialMediasProvider.scala | 2 +- .../scala/code/socialmedia/SocialMedia.scala | 13 +- 4 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 92f5ebfa2..5e18ee6f6 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -16,7 +16,7 @@ import code.kycdocuments.KycDocuments import code.kycmedias.KycMedias import code.kycstatuses.KycStatuses import code.kycchecks.KycChecks -import code.socialmedia.{SocialMedias, SocialMedia} +import code.socialmedia.{SocialMediaHandle, SocialMedia} import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer @@ -194,7 +194,7 @@ trait APIMethods200 { } // This contains an approach to surface the same resource via different end point in case of "one" bank. - // Experimental and might be removed. + // The "one" path is experimental and might be removed. lazy val privateAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for a single bank case "banks" :: BankId(bankId) :: "accounts" :: "private" :: Nil JsonGet json => { @@ -250,20 +250,20 @@ trait APIMethods200 { apiVersion, "getKycDocuments", "GET", - "/customers/CUSTOMER_NUMBER/kyc_document", - "Get documents for the logged in customer", - """Messages sent to the currently authenticated user. - | - |Authentication via OAuth is required.""", + "/customers/CUSTOMER_NUMBER/kyc_documents", + "Get KYC (know your customer) documents for a customer", + """Get a list of documents that affirm the identity of the customer + |Passport, driving licence etc. + |Authentication is required.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil) lazy val getKycDocuments : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_document" :: Nil JsonGet _ => { + case "customers" :: customerNumber :: "kyc_documents" :: Nil JsonGet _ => { user => { for { - u <- user ?~! "User must be logged in to retrieve customer messages" + u <- user ?~! "User must be logged in" cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycDocuments = KycDocuments.kycDocumentProvider.vend.getKycDocuments(cNumber) @@ -276,24 +276,24 @@ trait APIMethods200 { resourceDocs += ResourceDoc( - getKycMedias, + getKycMedia, apiVersion, - "getKycMedias", + "getKycMedia", "GET", "/customers/CUSTOMER_NUMBER/kyc_media", - "Get medias for the logged in customer", - """Messages sent to the currently authenticated user. + "Get KYC Media for a customer", + """Get media (scans, pictures, videos) that affirms the identity of the customer. | - |Authentication via OAuth is required.""", + |Authentication is required.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil) - lazy val getKycMedias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + lazy val getKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "customers" :: customerNumber :: "kyc_media" :: Nil JsonGet _ => { user => { for { - u <- user ?~! "User must be logged in to retrieve customer messages" + u <- user ?~! "User must be logged in" cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycMedias = KycMedias.kycMediaProvider.vend.getKycMedias(cNumber) @@ -305,9 +305,9 @@ trait APIMethods200 { } resourceDocs += ResourceDoc( - getKycCheck, + getKycChecks, apiVersion, - "getKycCheck", + "getKycChecks", "GET", "/customers/CUSTOMER_NUMBER/kyc_check", "Get checks for the logged in customer", @@ -318,11 +318,11 @@ trait APIMethods200 { emptyObjectJson, emptyObjectJson :: Nil) - lazy val getKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_check" :: Nil JsonGet _ => { + lazy val getKycChecks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "kyc_checks" :: Nil JsonGet _ => { user => { for { - u <- user ?~! "User must be logged in to retrieve customer messages" + u <- user ?~! "User must be logged in." cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycChecks = KycChecks.kycCheckProvider.vend.getKycChecks(cNumber) @@ -337,20 +337,20 @@ trait APIMethods200 { apiVersion, "getKycStatuses", "GET", - "/customers/CUSTOMER_NUMBER/kyc_status", - "Get statuses for the logged in customer", - """Messages sent to the currently authenticated user. + "/customers/CUSTOMER_NUMBER/kyc_statuses", + "Get the KYC statuses for a customer", + """Get the KYC statuses for a customer over time | - |Authentication via OAuth is required.""", + |Authentication is required.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil) lazy val getKycStatuses : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_status" :: Nil JsonGet _ => { + case "customers" :: customerNumber :: "kyc_statuses" :: Nil JsonGet _ => { user => { for { - u <- user ?~! "User must be logged in to retrieve customer messages" + u <- user ?~! "User must be logged in" cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycStatuses = KycStatuses.kycStatusProvider.vend.getKycStatuses(cNumber) @@ -362,27 +362,27 @@ trait APIMethods200 { } resourceDocs += ResourceDoc( - getSocialMedia, + getSocialMediaHandles, apiVersion, "getSocialMedia", "GET", - "/customers/CUSTOMER_NUMBER/social_media", - "Get social medias for the logged in customer", - """Messages sent to the currently authenticated user. + "/customers/CUSTOMER_NUMBER/social_media_handles", + "Get social media handles for a customer", + """Get social media handles for a customer. | |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil) - lazy val getSocialMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "social_media" :: Nil JsonGet _ => { + lazy val getSocialMediaHandles : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "customers" :: customerNumber :: "social_media_handles" :: Nil JsonGet _ => { user => { for { - u <- user ?~! "User must be logged in to retrieve customer messages" + u <- user ?~! "User must be logged in" cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { - val kycSocialMedias = SocialMedias.socialMediaProvider.vend.getSocialMedias(cNumber) + val kycSocialMedias = SocialMediaHandle.socialMediaHandleProvider.vend.getSocialMedias(cNumber) val json = JSONFactory.createSocialMediasJSON(kycSocialMedias) successJsonResponse(Extraction.decompose(json)) } @@ -398,7 +398,7 @@ trait APIMethods200 { apiVersion, "addKycDocument", "POST", - "/banks/BANK_ID/customer/kyc_document", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_documents", "Add a kyc_document for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), @@ -406,8 +406,11 @@ trait APIMethods200 { emptyObjectJson :: Nil ) + // TODO customerNumber should be in the url but not also in the postedData + lazy val addKycDocument : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customer" :: "kyc_document" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_documents" :: Nil JsonPost json -> _ => { + // customerNumber is duplicated in postedData. remove from that? user => { for { u <- user ?~! "User must be logged in to post Document" @@ -423,7 +426,7 @@ trait APIMethods200 { postedData.issue_date, postedData.issue_place, postedData.expiry_date), - "Server error: could not add message") + "Server error: could not add KycDocument") } yield { successJsonResponse(JsRaw("{}"), 201) } @@ -436,7 +439,7 @@ trait APIMethods200 { apiVersion, "addKycMedia", "POST", - "/banks/BANK_ID/customer/kyc_media", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_media", "Add a kyc_media for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycMediaJSON("73hyfgayt6ywerwerasd", "1239879", "image", "http://www.example.com/id-docs/123/image.png", exampleDate, "wuwjfuha234678", "98FRd987auhf87jab")), @@ -445,7 +448,8 @@ trait APIMethods200 { ) lazy val addKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customer" :: "kyc_media" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_media" :: Nil JsonPost json -> _ => { + // customerNumber is in url and duplicated in postedData. remove from that? user => { for { u <- user ?~! "User must be logged in to post Document" @@ -474,7 +478,7 @@ trait APIMethods200 { apiVersion, "addKycCheck", "POST", - "/banks/BANK_ID/customer/kyc_check", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_check", "Add a kyc_check for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycCheckJSON("98FRd987auhf87jab", "1239879", exampleDate, "online_meeting", "67876", "Simon Redfern", true, "")), @@ -483,7 +487,8 @@ trait APIMethods200 { ) lazy val addKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customer" :: "kyc_check" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_check" :: Nil JsonPost json -> _ => { + // customerNumber is in url and duplicated in postedData. remove from that? user => { for { u <- user ?~! "User must be logged in to post Document" @@ -513,7 +518,7 @@ trait APIMethods200 { apiVersion, "addKycStatus", "POST", - "/banks/BANK_ID/customer/kyc_status", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_statuses", "Add a kyc_status for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycStatusJSON("8762893876", true, exampleDate)), @@ -522,10 +527,11 @@ trait APIMethods200 { ) lazy val addKycStatus : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customer" :: "kyc_status" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_statuses" :: Nil JsonPost json -> _ => { + // customerNumber is in url and duplicated in postedData. remove from that? user => { for { - u <- user ?~! "User must be logged in to post Document" + u <- user ?~! "User must be logged" postedData <- tryo{json.extract[KycStatusJSON]} ?~! "Incorrect json format" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" @@ -543,34 +549,35 @@ trait APIMethods200 { } resourceDocs += ResourceDoc( - addSocialMedia, + addSocialMediaHandle, apiVersion, - "addSocialMedia", + "addSocialMediaHandle", "POST", - "/banks/BANK_ID/customer/social_media", - "Add a social_media for the customer specified by CUSTOMER_NUMBER", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media", + "Add a social_media_handle for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil ) - lazy val addSocialMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customer" :: "social_media" :: Nil JsonPost json -> _ => { + lazy val addSocialMediaHandle : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "social_media" :: Nil JsonPost json -> _ => { + // customerNumber is in url and duplicated in postedData. remove from that? user => { for { - u <- user ?~! "User must be logged in to post Document" + u <- user ?~! "User must be logged in" postedData <- tryo{json.extract[SocialMediaJSON]} ?~! "Incorrect json format" bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! "Customer not found" kycSocialMediaCreated <- booleanToBox( - SocialMedias.socialMediaProvider.vend.addSocialMedias( + SocialMediaHandle.socialMediaHandleProvider.vend.addSocialMedias( postedData.customer_number, postedData.`type`, postedData.handle, postedData.date_added, postedData.date_activated), - "Server error: could not add message") + "Server error: could not add") } yield { successJsonResponse(JsRaw("{}"), 201) } diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala index 78b7ac69e..ea3546b46 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala @@ -146,15 +146,15 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.privateAccountsAtOneBank, Implementations2_0_0.publicAccountsAtOneBank, Implementations2_0_0.getKycDocuments, - Implementations2_0_0.getKycMedias, + Implementations2_0_0.getKycMedia, Implementations2_0_0.getKycStatuses, - Implementations2_0_0.getKycCheck, - Implementations2_0_0.getSocialMedia, + Implementations2_0_0.getKycChecks, + Implementations2_0_0.getSocialMediaHandles, Implementations2_0_0.addKycDocument, Implementations2_0_0.addKycMedia, Implementations2_0_0.addKycStatus, Implementations2_0_0.addKycCheck, - Implementations2_0_0.addSocialMedia + Implementations2_0_0.addSocialMediaHandle ) routes.foreach(route => { diff --git a/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala b/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala index 524713368..c8f3a84dd 100644 --- a/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala +++ b/src/main/scala/code/socialmedia/MappedSocialMediasProvider.scala @@ -5,7 +5,7 @@ import code.model.dataAccess.APIUser import code.util.{DefaultStringField} import net.liftweb.mapper._ -object MappedSocialMediasProvider extends SocialMediaProvider { +object MappedSocialMediasProvider extends SocialMediaHandleProvider { override def getSocialMedias(customerNumber: String): List[MappedSocialMedia] = { MappedSocialMedia.findAll( diff --git a/src/main/scala/code/socialmedia/SocialMedia.scala b/src/main/scala/code/socialmedia/SocialMedia.scala index 3c9f18902..290dc699d 100644 --- a/src/main/scala/code/socialmedia/SocialMedia.scala +++ b/src/main/scala/code/socialmedia/SocialMedia.scala @@ -4,15 +4,18 @@ import java.util.Date import net.liftweb.util.SimpleInjector -object SocialMedias extends SimpleInjector { +// TODO Rename to SocialMediaHandle +object SocialMediaHandle extends SimpleInjector { - val socialMediaProvider = new Inject(buildOne _) {} + val socialMediaHandleProvider = new Inject(buildOne _) {} - def buildOne: SocialMediaProvider = MappedSocialMediasProvider + def buildOne: SocialMediaHandleProvider = MappedSocialMediasProvider } -trait SocialMediaProvider { + +// TODO Rename to SocialMediaHandlesProvider etc. +trait SocialMediaHandleProvider { def getSocialMedias(customerNumber: String) : List[SocialMedia] @@ -20,6 +23,8 @@ trait SocialMediaProvider { } + +// TODO Rename to SocialMediaHandle trait SocialMedia { def customerNumber : String def `type` : String From b0fc1d9d6960599acdbf6cb7b9ef68c68eb0882a Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 30 Jan 2016 00:41:55 +0000 Subject: [PATCH 328/702] renaming OBPAPI2.0.0.scala file to OBPAPI2_0_0.scala (reverting previous change) --- .../code/api/v2_0_0/{OBPAPI2.0.0.scala => OBPAPI2_0_0.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/scala/code/api/v2_0_0/{OBPAPI2.0.0.scala => OBPAPI2_0_0.scala} (100%) diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala similarity index 100% rename from src/main/scala/code/api/v2_0_0/OBPAPI2.0.0.scala rename to src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala From b1b24967c818463a3f40b56214c6f62ad5306b02 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 30 Jan 2016 00:54:37 +0000 Subject: [PATCH 329/702] Tweaking KYC urls --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 5e18ee6f6..9f1333f77 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -282,7 +282,7 @@ trait APIMethods200 { "GET", "/customers/CUSTOMER_NUMBER/kyc_media", "Get KYC Media for a customer", - """Get media (scans, pictures, videos) that affirms the identity of the customer. + """Get KYC media (scans, pictures, videos) that affirms the identity of the customer. | |Authentication is required.""", emptyObjectJson, @@ -309,8 +309,8 @@ trait APIMethods200 { apiVersion, "getKycChecks", "GET", - "/customers/CUSTOMER_NUMBER/kyc_check", - "Get checks for the logged in customer", + "/customers/CUSTOMER_NUMBER/kyc_checks", + "Get KYC checks for the logged in customer", """Messages sent to the currently authenticated user. | |Authentication via OAuth is required.""", @@ -399,7 +399,7 @@ trait APIMethods200 { "addKycDocument", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_documents", - "Add a kyc_document for the customer specified by CUSTOMER_NUMBER", + "Add a KYC document for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), emptyObjectJson, @@ -440,7 +440,7 @@ trait APIMethods200 { "addKycMedia", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_media", - "Add a kyc_media for the customer specified by CUSTOMER_NUMBER", + "Add some KYC media for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycMediaJSON("73hyfgayt6ywerwerasd", "1239879", "image", "http://www.example.com/id-docs/123/image.png", exampleDate, "wuwjfuha234678", "98FRd987auhf87jab")), emptyObjectJson, @@ -479,7 +479,7 @@ trait APIMethods200 { "addKycCheck", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_check", - "Add a kyc_check for the customer specified by CUSTOMER_NUMBER", + "Add a KYC check for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(KycCheckJSON("98FRd987auhf87jab", "1239879", exampleDate, "online_meeting", "67876", "Simon Redfern", true, "")), emptyObjectJson, @@ -554,7 +554,7 @@ trait APIMethods200 { "addSocialMediaHandle", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media", - "Add a social_media_handle for the customer specified by CUSTOMER_NUMBER", + "Add a social media handle for the customer specified by CUSTOMER_NUMBER", "", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), emptyObjectJson, From 2741f888d214a18d08883df2d42a92fcde8010e0 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 30 Jan 2016 12:13:04 +0100 Subject: [PATCH 330/702] Renamed jwtauth to directlogin --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- src/main/scala/code/api/OBPRestHelper.scala | 12 ++- .../api/{jwtauth.scala => directlogin.scala} | 98 +++++++------------ src/main/scala/code/api/util/APIUtil.scala | 31 ++++-- 4 files changed, 69 insertions(+), 76 deletions(-) rename src/main/scala/code/api/{jwtauth.scala => directlogin.scala} (66%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index ea9dadfc1..92799dbe5 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -187,8 +187,8 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(OAuthHandshake) // JWT auth endpoints - if(Props.getBool("allow_jwt_auth", true)) { - LiftRules.statelessDispatch.append(JWTAuth) + if(Props.getBool("allow_direct_login", true)) { + LiftRules.statelessDispatch.append(DirectLogin) } // Add the various API versions diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index e2176f9d3..255f64abb 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -43,6 +43,7 @@ import code.model.User import code.api.OAuthHandshake._ import net.liftweb.json.JsonAST.JValue import net.liftweb.json.Extraction +import net.liftweb.util.Props trait APIFailure{ val msg : String @@ -135,7 +136,16 @@ trait OBPRestHelper extends RestHelper with Loggable { case Failure(msg, _, _) => errorJsonResponse(msg) case _ => errorJsonResponse("oauth error") } - } else fn(Empty) + } else if (Props.getBool("allow_direct_login", true) && isItDirectLoginRequest) { + val user = DirectLogin.getUser + if (user.isDefined) { + fn(user) + } else { + fn(Empty) + } + } else { + fn(Empty) + } } class RichStringList(list: List[String]) { diff --git a/src/main/scala/code/api/jwtauth.scala b/src/main/scala/code/api/directlogin.scala similarity index 66% rename from src/main/scala/code/api/jwtauth.scala rename to src/main/scala/code/api/directlogin.scala index 9579b53a3..105aef7fb 100644 --- a/src/main/scala/code/api/jwtauth.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -46,22 +46,22 @@ import scala.compat.Platform * so they could authenticate their users. */ -object JWTAuth extends RestHelper with Loggable { +object DirectLogin extends RestHelper with Loggable { serve { //Handling get request for an "authorization token" case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest) => { - //Extract the jwtauth parameters from the header and test if the request is valid - var (httpCode, message, jwtAuthParameters) = validator("authorizationToken", "POST") + //Extract the directlogin parameters from the header and test if the request is valid + var (httpCode, message, directLoginParameters) = validator("authorizationToken", "POST") //Test if the request is valid if(httpCode==200) { - val claims = Map("app" -> "key") + val claims = Map("" -> "") val (token,secret) = generateTokenAndSecret(claims) //Save the token that we have generated - if(saveAuthorizationToken(jwtAuthParameters,token, secret)) + if(saveAuthorizationToken(directLoginParameters,token, secret)) message="token="+token } val headers = ("Content-type" -> "application/x-www-form-urlencoded") :: Nil @@ -72,25 +72,26 @@ object JWTAuth extends RestHelper with Loggable { //Check if the request (access token or request token) is valid and return a tuple def validator(requestType : String, httpMethod : String) : (Int, String, Map[String,String]) = { - //return a Map containing the jwtauth parameters : prameter -> value + //return a Map containing the directlogin parameters : prameter -> value def getAllParameters : Map[String,String]= { def toMapFromBasicAuth(paramsEncoded : String) = { //println("basic=" + paramsEncoded ) val params = new String(base64Decode(paramsEncoded.replaceAll("Basic ", ""))) params.toString.split(":") match { - case Array(str1, str2) => Map("username" -> str1, "password" -> str2) + case Array(str1, str2) => Map("dl_username" -> str1, "dl_password" -> str2) case _ => Map("error" -> "error") } } - //Convert the list of jwtauth parameters to a Map + //Convert the list of directlogin parameters to a Map def toMapFromReq(parametersList : Req ) = { - val jwtauthPossibleParameters = + val directloginPossibleParameters = List( - "username", - "password", - "token" + "dl_username", + "dl_password", + "dl_consumer_key", + "dl_token" ) if (parametersList.json_?) { @@ -120,14 +121,6 @@ object JWTAuth extends RestHelper with Loggable { } } - //check if the token exists and is still valid - def validRequestToken(tokenKey : String) ={ - Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Request)) match { - case Full(token) => token.isValid - case _ => false - } - } - def validAccessToken(tokenKey : String) = { Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Access)) match { case Full(token) => token.isValid @@ -136,14 +129,14 @@ object JWTAuth extends RestHelper with Loggable { } //@return the missing parameters depending of the request type - def missingjwtauthParameters(parameters : Map[String, String], requestType : String) : Set[String] = { + def missingdirectloginParameters(parameters : Map[String, String], requestType : String) : Set[String] = { //println(parameters.toString) if(requestType == "requestToken") - ("username" :: "password" :: List()).toSet diff parameters.keySet + ("dl_username" :: "dl_password" :: List()).toSet diff parameters.keySet else if(requestType=="authorizationToken") - ("username" :: "password" :: List()).toSet diff parameters.keySet + ("dl_username" :: "dl_password" :: "dl_consumer_key" :: List()).toSet diff parameters.keySet else if(requestType=="protectedResource") - ("token" :: List()).toSet diff parameters.keySet + ("dl_token" :: List()).toSet diff parameters.keySet else parameters.keySet } @@ -153,27 +146,19 @@ object JWTAuth extends RestHelper with Loggable { var parameters = getAllParameters - //are all the necessary jwtauth parameters present? - val missingParams = missingjwtauthParameters(parameters,requestType) + //are all the necessary directlogin parameters present? + val missingParams = missingdirectloginParameters(parameters,requestType) if( missingParams.size != 0 ) { message = "the following parameters are missing : " + missingParams.mkString(", ") httpCode = 400 } - - //In the case jwtauth authorization token request, check if the token is still valid and the verifier is correct - //else if(requestType=="authorizationToken" && !validRequestToken(parameters.get("token").get)) - //{ - // message = "Invalid or expired request token: " + parameters.get("token").get - // httpCode = 401 - //} - //In the case protected resource access request, check if the token is still valid else if ( requestType=="protectedResource" && - ! validAccessToken(parameters.get("token").get) + ! validAccessToken(parameters.get("dl_token").get) ) { - message = "Invalid or expired access token: " + parameters.get("token").get + message = "Invalid or expired access token: " + parameters.get("dl_token").get httpCode = 401 } //checking if the signature is correct @@ -202,31 +187,17 @@ object JWTAuth extends RestHelper with Loggable { (token_message, secret_message) } - private def saveAuthorizationToken(jwtauthParameters : Map[String, String], tokenKey : String, tokenSecret : String) = + private def saveAuthorizationToken(directloginParameters : Map[String, String], tokenKey : String, tokenSecret : String) = { import code.model.{Token, TokenType} - //val nonce = Nonce.create - //nonce.consumerkey(jwtauthParameters.get("consumer_key").get) - //nonce.timestamp(new Date(jwtauthParameters.get("timestamp").get.toLong)) - //nonce.tokenKey(jwtauthParameters.get("token").get) - //nonce.value(jwtauthParameters.get("nonce").get) - //val nonceSaved = nonce.save() - val token = Token.create token.tokenType(TokenType.Access) - //Consumer.find(By(Consumer.key,jwtauthParameters.get("consumer_key").get)) match { - // case Full(consumer) => token.consumerId(consumer.id) - // case _ => None - //} - //Token.find(By(Token.key, jwtauthParameters.get("token").get)) match { - val userId = OBPUser.getUserId(jwtauthParameters.get("username").get, jwtauthParameters.get("password").get) - //Token.find(By(Token.key, tokenKey)) match { - // case Full(requestToken) => token.userForeignKey(requestToken.userForeignKey) - // case _ => None - //} - token.verifier("verifier") - token.consumerId(1) //TESTING ONLY + Consumer.find(By(Consumer.key,directloginParameters.get("dl_consumer_key").get)) match { + case Full(consumer) => token.consumerId(consumer.id) + case _ => None + } + val userId = OBPUser.getUserId(directloginParameters.get("dl_username").get, directloginParameters.get("dl_password").get) token.userForeignKey(userId) token.key(tokenKey) token.secret(tokenSecret) @@ -237,8 +208,7 @@ object JWTAuth extends RestHelper with Loggable { token.insertDate(new Date(currentTime)) val tokenSaved = token.save() - //nonceSaved && - true && tokenSaved + tokenSaved } def getUser : Box[User] = { @@ -246,9 +216,9 @@ object JWTAuth extends RestHelper with Loggable { case Full(r) => r.request.method case _ => "GET" } - val (httpCode, message, jwtauthParameters) = validator("protectedResource", httpMethod) + val (httpCode, message, directloginParameters) = validator("protectedResource", httpMethod) - val user = getUser(200, jwtauthParameters.get("token")) + val user = getUser(200, directloginParameters.get("dl_token")) if (user != Empty ) { val res = Full(user.get) res @@ -261,15 +231,15 @@ object JWTAuth extends RestHelper with Loggable { if(httpCode==200) { import code.model.Token - logger.info("jwtauth header correct ") - Token.find(By(Token.key, tokenID.get)) match { + logger.info("directlogin header correct ") + Token.find(By(Token.key, tokenID.getOrElse(""))) match { case Full(token) => { logger.info("access token: "+ token + " found") val user = token.user //just a log user match { - case Full(u) => logger.info("user " + u.emailAddress + " was found from the jwtauth token") - case _ => logger.info("no user was found for the jwtauth token") + case Full(u) => logger.info("user " + u.emailAddress + " was found from the directlogin token") + case _ => logger.info("no user was found for the directlogin token") } user } diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index ba1509b97..afb502367 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -36,18 +36,12 @@ import code.api.v1_2.ErrorMessage import code.metrics.APIMetrics import code.model.User import net.liftweb.common.{Box, Full, Loggable} -import net.liftweb.http.{Req, JsonResponse, S} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.js.JsExp +import net.liftweb.http.{JsonResponse, Req, S} import net.liftweb.json.Extraction -import net.liftweb.json.JsonAST.{JObject, JValue} - - - - +import net.liftweb.json.JsonAST.{JNothing, JValue} import net.liftweb.util.Helpers._ - -import bootstrap.liftweb.Boot import net.liftweb.util.Props import scala.collection.JavaConversions.asScalaSet @@ -65,6 +59,25 @@ object APIUtil extends Loggable { case _ => "GET" } + def isItDirectLoginRequest : Boolean = { + S.request match { + case Full(a) => a.json match { + case Full(json) => { + if ((json \\ "dl_token") != JNothing && + (json \\ "dl_token").toString.matches( """^\w.*\.\w.*\.\w.*$""")) + true + else if ((json \\ "dl_password") != JNothing && + (json \\ "dl_username") != JNothing) + false + else + false + } + case _ => false + } + case _ => false + } + } + def isThereAnOAuthHeader : Boolean = { S.request match { case Full(a) => a.header("Authorization") match { @@ -133,7 +146,7 @@ object APIUtil extends Loggable { import org.apache.http.protocol.HTTP.UTF_8 import scala.collection.Map - import scala.collection.immutable.{TreeMap, Map => IMap} + import scala.collection.immutable.{Map => IMap, TreeMap} import scala.collection.mutable.Set case class Consumer(key: String, secret: String) From 5c573a944d6d3f7b8be91a4e457e4f85a4b5f795 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 30 Jan 2016 14:33:33 +0100 Subject: [PATCH 331/702] Added isThereDirectLoginHeader to APIUtil --- src/main/scala/code/api/util/APIUtil.scala | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index afb502367..ce3c8c945 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -40,7 +40,7 @@ import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.js.JsExp import net.liftweb.http.{JsonResponse, Req, S} import net.liftweb.json.Extraction -import net.liftweb.json.JsonAST.{JNothing, JValue} +import net.liftweb.json.JsonAST.JValue import net.liftweb.util.Helpers._ import net.liftweb.util.Props @@ -59,19 +59,10 @@ object APIUtil extends Loggable { case _ => "GET" } - def isItDirectLoginRequest : Boolean = { + def isThereDirectLoginHeader : Boolean = { S.request match { - case Full(a) => a.json match { - case Full(json) => { - if ((json \\ "dl_token") != JNothing && - (json \\ "dl_token").toString.matches( """^\w.*\.\w.*\.\w.*$""")) - true - else if ((json \\ "dl_password") != JNothing && - (json \\ "dl_username") != JNothing) - false - else - false - } + case Full(a) => a.header("Authorization") match { + case Full(parameters) => parameters.contains("DirectLogin") case _ => false } case _ => false From 7eb75133c9ecc06b5aa5820791dcb436a785fb18 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 30 Jan 2016 14:39:41 +0100 Subject: [PATCH 332/702] Added isThereDirectLoginHeader to APIUtil --- src/main/scala/code/api/OBPRestHelper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index 255f64abb..344663f2e 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -136,7 +136,7 @@ trait OBPRestHelper extends RestHelper with Loggable { case Failure(msg, _, _) => errorJsonResponse(msg) case _ => errorJsonResponse("oauth error") } - } else if (Props.getBool("allow_direct_login", true) && isItDirectLoginRequest) { + } else if (Props.getBool("allow_direct_login", true) && isThereDirectLoginHeader) { val user = DirectLogin.getUser if (user.isDefined) { fn(user) From 949ca36ae24633b07854c120fa4a01cca5d8735b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 30 Jan 2016 14:22:18 +0000 Subject: [PATCH 333/702] Tweaks to counterparty load --- .../code/sandbox/ImportCounterpartyMetadata.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/test/scala/code/sandbox/ImportCounterpartyMetadata.scala b/src/test/scala/code/sandbox/ImportCounterpartyMetadata.scala index 9e7388d42..b91f7927d 100644 --- a/src/test/scala/code/sandbox/ImportCounterpartyMetadata.scala +++ b/src/test/scala/code/sandbox/ImportCounterpartyMetadata.scala @@ -42,17 +42,25 @@ import code.api.ObpJson.BarebonesAccountsJson case class CounterpartyJSONRecord(name: String, category: String, superCategory: String, logoUrl: String, homePageUrl: String, region: String) case class UserJSONRecord(email: String, password: String, display_name: String) + +// Import counterparty metadata +// Instructions for using this: +// Run a copy of the API somewhere (else) +// Set the paths for users and counterparties. (remove the outer [] from the json) + + object ImportCounterpartyMetadata extends SendServerRequests { def main(args : Array[String]) { implicit val formats = DefaultFormats //load json for counterpaties - var path = "/Users/stefan/Downloads/OBP_sandbox_counterparties_pretty.json" + var path = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/ulster_bank_2016/OBP_sandbox_counterparties_pretty_MOD.json" var records = JsonParser.parse(Source.fromFile(path) mkString) var counterparties = ListBuffer[CounterpartyJSONRecord]() //collect counterparties records for(r <- records.children){ + //logger.info(s" extract counterparty records") val rec = r.extract[CounterpartyJSONRecord] //println (rec.name + "in region " + rec.region) counterparties.append(rec) @@ -61,7 +69,7 @@ object ImportCounterpartyMetadata extends SendServerRequests { println("Got " + counterparties.length + " counterparty records") //load sandbox users from json - path = "/Users/stefan/Downloads/OBP_sandbox_pretty_load_002.json" + path = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/ulster_bank_2016/loaded_30_jan_UB_OBP_sandbox_pretty.json" records = JsonParser.parse(Source.fromFile(path) mkString) val users = (records \ "users").children println("got " + users.length + " users") From 02202e09172b82df038ab55fab38499008baf1f8 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 30 Jan 2016 15:29:02 +0100 Subject: [PATCH 334/702] Complete rewrite of DirectLogin authorization logic --- src/main/scala/code/api/directlogin.scala | 44 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 105aef7fb..7cc68966d 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -84,6 +84,36 @@ object DirectLogin extends RestHelper with Loggable { } } + def toMap(parametersList : String) = { + //transform the string "directlogin_prameter="value"" + //to a tuple (directlogin_parameter,Decoded(value)) + def dynamicListExtract(input: String) = { + val directloginPossibleParameters = + List( + "dl_consumer_key", + "dl_token", + "dl_username", + "dl_password" + ) + + if (input contains "=") { + val split = input.split("=",2) + val parameterValue = split(1).replace("\"","") + //add only OAuth parameters and not empty + if(directloginPossibleParameters.contains(split(0)) && ! parameterValue.isEmpty) + Some(split(0),parameterValue) // return key , value + else + None + } + else + None + } + //we delete the "Oauth" prefix and all the white spaces that may exist in the string + val cleanedParameterList = parametersList.stripPrefix("DirectLogin").replaceAll("\\s","") + val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) + params + } + //Convert the list of directlogin parameters to a Map def toMapFromReq(parametersList : Req ) = { val directloginPossibleParameters = @@ -106,12 +136,18 @@ object DirectLogin extends RestHelper with Loggable { } S.request match { - case Full(a) => a.header("Authorization") match { - case Full(parameters) => toMapFromBasicAuth(parameters) - case _ => toMapFromReq(a) + case Full(a) => a.header("Authorization") match { + case Full(header) => { + if (header.contains("DirectLogin")) + toMap(header) + else + toMapFromBasicAuth(header) } - case _ => Map("error" -> "request incorrect") + case _ => toMapFromReq(a) + } + case _ => Map("error" -> "request incorrect") } + } def registeredApplication(consumerKey : String ) : Boolean = { From 581ac82e40664283df2830dd1d14ecc4a16c5652 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 30 Jan 2016 15:52:22 +0100 Subject: [PATCH 335/702] Looks like consumer_key is not needed for DirectLogin to function --- src/main/scala/code/api/directlogin.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 7cc68966d..992d20f80 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -170,7 +170,8 @@ object DirectLogin extends RestHelper with Loggable { if(requestType == "requestToken") ("dl_username" :: "dl_password" :: List()).toSet diff parameters.keySet else if(requestType=="authorizationToken") - ("dl_username" :: "dl_password" :: "dl_consumer_key" :: List()).toSet diff parameters.keySet +// ("dl_username" :: "dl_password" :: "dl_consumer_key" :: List()).toSet diff parameters.keySet + ("dl_username" :: "dl_password" :: List()).toSet diff parameters.keySet else if(requestType=="protectedResource") ("dl_token" :: List()).toSet diff parameters.keySet else @@ -229,10 +230,10 @@ object DirectLogin extends RestHelper with Loggable { val token = Token.create token.tokenType(TokenType.Access) - Consumer.find(By(Consumer.key,directloginParameters.get("dl_consumer_key").get)) match { - case Full(consumer) => token.consumerId(consumer.id) - case _ => None - } + //Consumer.find(By(Consumer.key,directloginParameters.get("dl_consumer_key").get)) match { + // case Full(consumer) => token.consumerId(consumer.id) + // case _ => None + //} val userId = OBPUser.getUserId(directloginParameters.get("dl_username").get, directloginParameters.get("dl_password").get) token.userForeignKey(userId) token.key(tokenKey) From 873b369a62735eb00f96a22092209005061398cd Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 8 Feb 2016 17:41:32 +0100 Subject: [PATCH 336/702] Rewrite of directlogin --- src/main/scala/code/api/directlogin.scala | 179 ++++++++++++---------- 1 file changed, 98 insertions(+), 81 deletions(-) diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 992d20f80..5663d03a8 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -30,11 +30,13 @@ package code.api import java.util.Date import authentikat.jwt.{JsonWebToken, JwtClaimsSet, JwtHeader} +import code.api.util.APIUtil._ import code.model.dataAccess.OBPUser import code.model.{Consumer, Token, TokenType, User} import net.liftweb.common._ import net.liftweb.http._ import net.liftweb.http.rest.RestHelper +import net.liftweb.json.Extraction import net.liftweb.mapper.By import net.liftweb.util.Helpers import net.liftweb.util.Helpers._ @@ -46,14 +48,49 @@ import scala.compat.Platform * so they could authenticate their users. */ + +object JSONFactory { + case class TokenJSON( token : String ) + + def stringOrNull(text: String) = + if (text == null || text.isEmpty) + null + else + text + + def stringOptionOrNull(text: Option[String]) = + text match { + case Some(t) => stringOrNull(t) + case _ => null + } + + def createTokenJSON(token: String): TokenJSON = { + new TokenJSON( + stringOrNull(token) + ) + } +} + object DirectLogin extends RestHelper with Loggable { - serve + + def dlServe(handler : PartialFunction[Req, JsonResponse]) : Unit = { + val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = { + new PartialFunction[Req, () => Box[LiftResponse]] { + def apply(r : Req) = { + handler(r) + } + def isDefinedAt(r : Req) = handler.isDefinedAt(r) + } + } + super.serve(obpHandler) + } + + dlServe { //Handling get request for an "authorization token" - case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest) => - { - //Extract the directlogin parameters from the header and test if the request is valid - var (httpCode, message, directLoginParameters) = validator("authorizationToken", "POST") + case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest|GetRequest) => { + //Extract the directLogin parameters from the header and test if the request is valid + var (httpCode, message, directLoginParameters) = validator("authorizationToken", getHttpMethod) //Test if the request is valid if(httpCode==200) { @@ -62,45 +99,49 @@ object DirectLogin extends RestHelper with Loggable { //Save the token that we have generated if(saveAuthorizationToken(directLoginParameters,token, secret)) - message="token="+token + message = token + else + message = "invalid" } - val headers = ("Content-type" -> "application/x-www-form-urlencoded") :: Nil - //return an HTTP response - Full(InMemoryResponse(message.getBytes,headers,Nil,httpCode)) + val tokenJSON = JSONFactory.createTokenJSON(message) + successJsonResponse(Extraction.decompose(tokenJSON)) } } + def getHttpMethod = S.request match { + case Full(s) => { + if (s.post_?) + "POST" + else if (s.get_?) + "GET" + else if (s.put_?) + "PUT" + else + "UNKNOWN" + } + case _ => "ERROR" + } + //Check if the request (access token or request token) is valid and return a tuple def validator(requestType : String, httpMethod : String) : (Int, String, Map[String,String]) = { - //return a Map containing the directlogin parameters : prameter -> value + //return a Map containing the directLogin parameters : prameter -> value def getAllParameters : Map[String,String]= { - - def toMapFromBasicAuth(paramsEncoded : String) = { - //println("basic=" + paramsEncoded ) - val params = new String(base64Decode(paramsEncoded.replaceAll("Basic ", ""))) - params.toString.split(":") match { - case Array(str1, str2) => Map("dl_username" -> str1, "dl_password" -> str2) - case _ => Map("error" -> "error") - } - } - def toMap(parametersList : String) = { - //transform the string "directlogin_prameter="value"" - //to a tuple (directlogin_parameter,Decoded(value)) + //transform the string "directLogin_prameter="value"" + //to a tuple (directLogin_parameter,Decoded(value)) def dynamicListExtract(input: String) = { - val directloginPossibleParameters = + val directLoginPossibleParameters = List( - "dl_consumer_key", - "dl_token", - "dl_username", - "dl_password" + "consumer_key", + "token", + "username", + "password" ) - if (input contains "=") { val split = input.split("=",2) val parameterValue = split(1).replace("\"","") //add only OAuth parameters and not empty - if(directloginPossibleParameters.contains(split(0)) && ! parameterValue.isEmpty) + if(directLoginPossibleParameters.contains(split(0)) && ! parameterValue.isEmpty) Some(split(0),parameterValue) // return key , value else None @@ -113,27 +154,6 @@ object DirectLogin extends RestHelper with Loggable { val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) params } - - //Convert the list of directlogin parameters to a Map - def toMapFromReq(parametersList : Req ) = { - val directloginPossibleParameters = - List( - "dl_username", - "dl_password", - "dl_consumer_key", - "dl_token" - ) - - if (parametersList.json_?) { - val parameters = parametersList.json.map( _ .values ).getOrElse(Map[String,String] _) - parameters.asInstanceOf[Map[String,String]] - } else if (parametersList.xml_?) { - val parameters = parametersList.xml.map( _ .text ).getOrElse(Map[String,String] _) - parameters.asInstanceOf[Map[String,String]] - } else { - Map("error" -> "parameters incorrect") - } - } S.request match { case Full(a) => a.header("Authorization") match { @@ -141,9 +161,9 @@ object DirectLogin extends RestHelper with Loggable { if (header.contains("DirectLogin")) toMap(header) else - toMapFromBasicAuth(header) + Map("error" -> "header incorrect") } - case _ => toMapFromReq(a) + case _ => Map("error" -> "missing header") } case _ => Map("error" -> "request incorrect") } @@ -165,15 +185,12 @@ object DirectLogin extends RestHelper with Loggable { } //@return the missing parameters depending of the request type - def missingdirectloginParameters(parameters : Map[String, String], requestType : String) : Set[String] = { + def missingDirectLoginParameters(parameters : Map[String, String], requestType : String) : Set[String] = { //println(parameters.toString) - if(requestType == "requestToken") - ("dl_username" :: "dl_password" :: List()).toSet diff parameters.keySet - else if(requestType=="authorizationToken") -// ("dl_username" :: "dl_password" :: "dl_consumer_key" :: List()).toSet diff parameters.keySet - ("dl_username" :: "dl_password" :: List()).toSet diff parameters.keySet + if(requestType=="authorizationToken") + ("username" :: "password" :: "consumer_key" :: List()).toSet diff parameters.keySet else if(requestType=="protectedResource") - ("dl_token" :: List()).toSet diff parameters.keySet + ("token" :: List()).toSet diff parameters.keySet else parameters.keySet } @@ -181,21 +198,21 @@ object DirectLogin extends RestHelper with Loggable { var message = "" var httpCode : Int = 500 - var parameters = getAllParameters + val parameters = getAllParameters - //are all the necessary directlogin parameters present? - val missingParams = missingdirectloginParameters(parameters,requestType) - if( missingParams.size != 0 ) + //are all the necessary directLogin parameters present? + val missingParams = missingDirectLoginParameters(parameters,requestType) + if( missingParams.nonEmpty ) { message = "the following parameters are missing : " + missingParams.mkString(", ") httpCode = 400 } else if ( requestType=="protectedResource" && - ! validAccessToken(parameters.get("dl_token").get) + ! validAccessToken(parameters.get("token").get) ) { - message = "Invalid or expired access token: " + parameters.get("dl_token").get + message = "Invalid or expired access token: " + parameters.get("token").get httpCode = 401 } //checking if the signature is correct @@ -208,7 +225,6 @@ object DirectLogin extends RestHelper with Loggable { httpCode = 200 if(message.nonEmpty) logger.error("error message : " + message) - (httpCode, message, parameters) } @@ -220,21 +236,24 @@ object DirectLogin extends RestHelper with Loggable { val header = JwtHeader("HS256") // generate jwt token val token_message = JsonWebToken(header, JwtClaimsSet(claims), secret_message) - (token_message, secret_message) } - private def saveAuthorizationToken(directloginParameters : Map[String, String], tokenKey : String, tokenSecret : String) = + private def saveAuthorizationToken(directLoginParameters : Map[String, String], tokenKey : String, tokenSecret : String) = { import code.model.{Token, TokenType} - val token = Token.create token.tokenType(TokenType.Access) - //Consumer.find(By(Consumer.key,directloginParameters.get("dl_consumer_key").get)) match { - // case Full(consumer) => token.consumerId(consumer.id) - // case _ => None - //} - val userId = OBPUser.getUserId(directloginParameters.get("dl_username").get, directloginParameters.get("dl_password").get) + Consumer.find(By(Consumer.key,directLoginParameters.get("consumer_key").get)) match { + case Full(consumer) => token.consumerId(consumer.id) + case _ => None + } + val username = directLoginParameters.get("username").get.toString + val password = directLoginParameters.get("password").get match { + case p: String => p + case _ => "error" + } + val userId = OBPUser.getUserId(username, password) token.userForeignKey(userId) token.key(tokenKey) token.secret(tokenSecret) @@ -244,7 +263,6 @@ object DirectLogin extends RestHelper with Loggable { token.expirationDate(new Date(currentTime+tokenDuration)) token.insertDate(new Date(currentTime)) val tokenSaved = token.save() - tokenSaved } @@ -253,9 +271,8 @@ object DirectLogin extends RestHelper with Loggable { case Full(r) => r.request.method case _ => "GET" } - val (httpCode, message, directloginParameters) = validator("protectedResource", httpMethod) - - val user = getUser(200, directloginParameters.get("dl_token")) + val (httpCode, message, directLoginParameters) = validator("protectedResource", httpMethod) + val user = getUser(200, directLoginParameters.get("token")) if (user != Empty ) { val res = Full(user.get) res @@ -268,15 +285,15 @@ object DirectLogin extends RestHelper with Loggable { if(httpCode==200) { import code.model.Token - logger.info("directlogin header correct ") + logger.info("directLogin header correct ") Token.find(By(Token.key, tokenID.getOrElse(""))) match { case Full(token) => { logger.info("access token: "+ token + " found") val user = token.user //just a log user match { - case Full(u) => logger.info("user " + u.emailAddress + " was found from the directlogin token") - case _ => logger.info("no user was found for the directlogin token") + case Full(u) => logger.info("user " + u.emailAddress + " was found from the directLogin token") + case _ => logger.info("no user was found for the directLogin token") } user } From 321096663a2e84b223c3f4dc268cc4760fecac0c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 8 Feb 2016 17:56:57 +0100 Subject: [PATCH 337/702] Resource Doc text tweak --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index cc9f72bf9..2bc5891fc 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -2073,7 +2073,7 @@ Authentication via OAuth is required if the view is not public.""", "deleteImageForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images/IMAGE_ID", - "delete an image", + "Delete an image", """Deletes the image IMAGE_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the image.""", From 4e9a4210d6f855e6068f6f56b16b1b28a99f36fa Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 8 Feb 2016 21:58:11 +0100 Subject: [PATCH 338/702] Adding isCore, isPSD2 flags and tags to ResourceDocs --- .../ResourceDocsAPIMethods.scala | 15 +- src/main/scala/code/api/util/APIUtil.scala | 25 +- .../scala/code/api/v1_2_1/APIMethods121.scala | 364 ++++++++++++++---- .../scala/code/api/v1_3_0/APIMethods130.scala | 10 +- .../scala/code/api/v1_4_0/APIMethods140.scala | 65 +++- .../scala/code/api/v2_0_0/APIMethods200.scala | 83 +++- 6 files changed, 454 insertions(+), 108 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 96e65e2b0..f9e1f4cfa 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -102,7 +102,10 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods """, emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagApiInfo) ) // Provides resource documents so that API Explorer (or other apps) can display API documentation @@ -141,7 +144,10 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods """, emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagApiInfo) ) def getResourceDocsSwagger : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -200,7 +206,10 @@ trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods |_etc_...""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagApiInfo)) } diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index ce3c8c945..c8ad8aeb1 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -271,6 +271,26 @@ object APIUtil extends Loggable { TODO Can we extract apiVersion, apiFunction, requestVerb and requestUrl from partialFunction? */ + + case class ResourceDocTag(tag: String) + + val apiTagPayment = ResourceDocTag("Payments") + val apiTagApiInfo = ResourceDocTag("API Info") + val apiTagBanks = ResourceDocTag("Banks") + val apiTagAccounts = ResourceDocTag("Accounts") + val apiTagPublicData = ResourceDocTag("Public Data") + val apiTagPrivateData = ResourceDocTag("Private Data") + val apiTagTransactions = ResourceDocTag("Transactions") + val apiTagMetaData = ResourceDocTag("Meta Data") + val apiTagViews = ResourceDocTag("Views") + val apiTagEntitlements = ResourceDocTag("Entitlements") + val apiTagOwnerRequired = ResourceDocTag("Owner View Required") + val apiTagCounterparties = ResourceDocTag("Counterparties") + val apiTagKyc = ResourceDocTag("KYC") + val apiTagCustomer = ResourceDocTag("Customer") + + + case class ResourceDoc( partialFunction : PartialFunction[Req, Box[User] => Box[JsonResponse]], apiVersion: String, // TODO: Constrain to certain strings? @@ -281,7 +301,10 @@ object APIUtil extends Loggable { description: String, // Longer description (originally taken from github wiki) exampleRequestBody: JValue, // An example of the body required (maybe empty) successResponseBody: JValue, // A successful response body - errorResponseBodies: List[JValue]) // Possible error responses + errorResponseBodies: List[JValue], // Possible error responses + isCore: Boolean, + isPSD2: Boolean, + tags: List[ResourceDocTag]) def authenticationRequiredMessage(authRequired: Boolean) : String = authRequired match { diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 2bc5891fc..083b349b9 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -10,7 +10,7 @@ import APIUtil._ import net.liftweb.util.Helpers._ import net.liftweb.http.rest.RestHelper import java.net.URL -import net.liftweb.util.Props +import net.liftweb.util.{True, Props} import code.bankconnectors._ import code.bankconnectors.OBPOffset import code.bankconnectors.OBPFromDate @@ -24,6 +24,11 @@ import scala.collection.mutable.ArrayBuffer // Makes JValue assignment to Nil work import net.liftweb.json.JsonDSL._ + + + + + case class MakePaymentJson( bank_id : String, account_id : String, @@ -73,7 +78,6 @@ trait APIMethods121 { val emptyObjectJson : JValue = Nil val apiVersion : String = "1_2_1" - resourceDocs += ResourceDoc( root(apiVersion), apiVersion, @@ -88,7 +92,10 @@ trait APIMethods121 { |* Git Commit""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + false, + apiTagApiInfo :: Nil) def root(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case Nil JsonGet json => { @@ -119,7 +126,10 @@ trait APIMethods121 { |* Website""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + false, + apiTagBanks :: Nil) lazy val allBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get banks @@ -152,7 +162,10 @@ trait APIMethods121 { |* Website""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + false, + apiTagBanks :: Nil) lazy val bankById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -189,7 +202,10 @@ trait APIMethods121 { |""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + apiTagAccounts :: Nil) lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for all banks (private + public) @@ -212,7 +228,10 @@ trait APIMethods121 { |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + apiTagAccounts :: Nil) lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for all banks @@ -238,7 +257,10 @@ trait APIMethods121 { |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + apiTagAccounts :: Nil) lazy val publicAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for all banks @@ -267,7 +289,10 @@ trait APIMethods121 { """, emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + apiTagAccounts :: Nil) lazy val allAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for a single bank (private + public) @@ -295,7 +320,10 @@ trait APIMethods121 { |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + apiTagAccounts :: Nil) lazy val privateAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for a single bank @@ -323,7 +351,10 @@ trait APIMethods121 { |Authentication via OAuth is not required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + apiTagAccounts :: apiTagPublicData :: Nil) lazy val publicAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for a single bank @@ -359,7 +390,10 @@ trait APIMethods121 { |OAuth authentication is required if the 'is_public' field in view (VIEW_ID) is not set to `true`.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + apiTagAccounts :: Nil) lazy val accountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get account by id @@ -388,7 +422,10 @@ trait APIMethods121 { "", Extraction.decompose(UpdateAccountJSON("ACCOUNT_ID of the account we want to update", "New label", "BANK_ID")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagMetaData)) lazy val updateAccountLabel : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //change account label @@ -440,7 +477,10 @@ trait APIMethods121 { |OAuth authentication is required and the user needs to have access to the owner view.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews)) lazy val getViewsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get the available views on an bank account @@ -479,7 +519,10 @@ trait APIMethods121 { | The 'allowed_actions' field is a list containing the name of the actions allowed on this view, all the actions contained will be set to `true` on the view creation, the rest will be set to `false`.""", Extraction.decompose(ViewCreationJSON("Name of view to create", "Description of view (this example is public, uses the public alias, and has limited access to account data)", true, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label", "can_see_tags"))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews)) lazy val createViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //creates a view on an bank account @@ -512,7 +555,10 @@ trait APIMethods121 { |of a view is not editable (it is only set when a view is created)""", Extraction.decompose(ViewUpdateData("New description of view", false, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label"))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews)) lazy val updateViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //updates a view on a bank account @@ -540,7 +586,10 @@ trait APIMethods121 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews)) lazy val deleteViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //deletes a view on an bank account @@ -566,7 +615,11 @@ trait APIMethods121 { |OAuth authentication is required and the user needs to have access to the owner view.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews, apiTagEntitlements) + ) lazy val getPermissionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get access @@ -596,7 +649,10 @@ trait APIMethods121 { |OAuth authentication is required and the user needs to have access to the owner view.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews, apiTagEntitlements)) lazy val getPermissionForUserForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get access for specific user @@ -627,7 +683,10 @@ trait APIMethods121 { |OAuth authentication is required and the user needs to have access to the owner view.""", Extraction.decompose(ViewIdsJson(List("owner","auditor","investor"))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) lazy val addPermissionForUserForBankAccountForMultipleViews : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a list of views @@ -659,7 +718,10 @@ trait APIMethods121 { |Granting access to a public view will return an error message, as the user already has access.""", emptyObjectJson, // No Json body required emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) lazy val addPermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a specific view @@ -691,7 +753,10 @@ trait APIMethods121 { |OAuth authentication is required and the user needs to have access to the owner view.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) lazy val removePermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete access for specific user to one view @@ -718,7 +783,10 @@ trait APIMethods121 { |OAuth authentication is required and the user needs to have access to the owner view.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) lazy val removePermissionForUserForBankAccountForAllViews : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete access for specific user to all the views @@ -745,7 +813,10 @@ trait APIMethods121 { |OAuth authentication is required if the view VIEW_ID is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagCounterparties)) lazy val getCounterpartiesForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get other accounts for one account @@ -774,7 +845,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagCounterparties)) lazy val getCounterpartyByIdForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get one other account by id @@ -803,7 +877,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val getCounterpartyMetadata : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get metadata of one other account @@ -833,7 +910,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val getCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public alias of other bank account @@ -869,7 +949,10 @@ trait APIMethods121 { |The VIEW_ID parameter should be a view the caller is permitted to access to and that has permission to create public aliases.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add public alias to other bank account @@ -901,7 +984,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update public alias of other bank account @@ -933,7 +1019,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete public alias of other bank account @@ -962,7 +1051,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val getCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private alias of other bank account @@ -993,7 +1085,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add private alias to other bank account @@ -1026,7 +1121,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update private alias of other bank account @@ -1059,7 +1157,10 @@ trait APIMethods121 { |OAuth authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete private alias of other bank account @@ -1088,7 +1189,10 @@ trait APIMethods121 { "Add a description of the counter party from the perpestive of the account e.g. My dentist.", Extraction.decompose(MoreInfoJSON("More info")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add more info to other bank account @@ -1119,7 +1223,10 @@ trait APIMethods121 { "Update the description of the counter party from the perpestive of the account e.g. My dentist.", Extraction.decompose(MoreInfoJSON("More info")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update more info of other bank account @@ -1150,7 +1257,10 @@ trait APIMethods121 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete more info of other bank account @@ -1179,7 +1289,10 @@ trait APIMethods121 { "A url which represents the counterparty (home page url etc.)", Extraction.decompose(UrlJSON("www.example.com")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1211,7 +1324,10 @@ trait APIMethods121 { "A url which represents the counterparty (home page url etc.)", Extraction.decompose(UrlJSON("www.example.com")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update url of other bank account @@ -1242,7 +1358,10 @@ trait APIMethods121 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete url of other bank account @@ -1271,7 +1390,10 @@ trait APIMethods121 { "Add a url that points to the logo of the counterparty", Extraction.decompose(ImageUrlJSON("www.example.com/logo.png")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add image url to other bank account @@ -1302,7 +1424,10 @@ trait APIMethods121 { "Update the url that points to the logo of the counterparty", Extraction.decompose(ImageUrlJSON("www.example.com/logo.png")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update image url of other bank account @@ -1333,7 +1458,10 @@ trait APIMethods121 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete image url of other bank account @@ -1362,7 +1490,10 @@ trait APIMethods121 { "", Extraction.decompose(OpenCorporateUrlJSON("https://opencorporates.com/companies/gb/04351490")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add open corporate url to other bank account @@ -1393,7 +1524,10 @@ trait APIMethods121 { "", Extraction.decompose(OpenCorporateUrlJSON("https://opencorporates.com/companies/gb/04351490")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update open corporate url of other bank account @@ -1424,7 +1558,10 @@ trait APIMethods121 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete open corporate url of other bank account @@ -1453,7 +1590,10 @@ trait APIMethods121 { "Add the geolocation of the counterparty's registered address", Extraction.decompose(CorporateLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add corporate location to other bank account @@ -1486,7 +1626,10 @@ trait APIMethods121 { "Update the geolocation of the counterparty's registered address", Extraction.decompose(CorporateLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update corporate location of other bank account @@ -1519,7 +1662,10 @@ trait APIMethods121 { "Delete the geolocation of the counterparty's registered address", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete corporate location of other bank account @@ -1553,7 +1699,10 @@ trait APIMethods121 { "Add geocoordinates of the counterparty's main location", Extraction.decompose(PhysicalLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val addCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add physical location to other bank account @@ -1586,7 +1735,10 @@ trait APIMethods121 { "Update geocoordinates of the counterparty's main location", Extraction.decompose(PhysicalLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val updateCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update physical location to other bank account @@ -1619,7 +1771,10 @@ trait APIMethods121 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCounterparties, apiTagMetaData)) lazy val deleteCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete physical location of other bank account @@ -1664,7 +1819,10 @@ trait APIMethods121 { |**Date format parameter**: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" (2014-07-01T00:00:00.000Z) ==> time zone is UTC.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagAccounts, apiTagTransactions)) lazy val getTransactionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get transactions @@ -1695,7 +1853,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagAccounts, apiTagTransactions)) lazy val getTransactionByIdForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get transaction by id @@ -1724,7 +1885,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val getTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get narrative @@ -1752,7 +1916,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", Extraction.decompose(TransactionNarrativeJSON("My new (old!) piano")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val addTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add narrative @@ -1783,7 +1950,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", Extraction.decompose(TransactionNarrativeJSON("My new (old!) piano")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val updateTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update narrative @@ -1814,7 +1984,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val deleteTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete narrative @@ -1842,7 +2015,10 @@ trait APIMethods121 { |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val getCommentsForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get comments @@ -1870,7 +2046,10 @@ trait APIMethods121 { |OAuth authentication is required since the comment is linked with the user.""", Extraction.decompose(PostTransactionCommentJSON("Why did we spend money on this again?")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val addCommentForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add comment @@ -1902,7 +2081,10 @@ trait APIMethods121 { |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the comment.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val deleteCommentForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete comment @@ -1930,7 +2112,10 @@ trait APIMethods121 { Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val getTagsForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get tags @@ -1958,7 +2143,10 @@ Authentication via OAuth is required if the view is not public.""", |OAuth authentication is required since the tag is linked with the user.""", Extraction.decompose(PostTransactionTagJSON("holiday")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val addTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add a tag @@ -1991,7 +2179,10 @@ Authentication via OAuth is required if the view is not public.""", Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the tag.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val deleteTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete a tag @@ -2020,7 +2211,10 @@ Authentication via OAuth is required. The user must either have owner privileges Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val getImagesForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get images @@ -2048,7 +2242,10 @@ Authentication via OAuth is required if the view is not public.""", |OAuth authentication is required since the image is linked with the user.""", Extraction.decompose(PostTransactionImageJSON("The new printer", "www.example.com/images/printer.png")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val addImageForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add an image @@ -2079,7 +2276,10 @@ Authentication via OAuth is required if the view is not public.""", |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the image.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val deleteImageForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete an image @@ -2108,7 +2308,10 @@ Authentication via OAuth is required if the view is not public.""", |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val getWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get where tag @@ -2137,7 +2340,10 @@ Authentication via OAuth is required if the view is not public.""", |OAuth authentication is required since the geo tag is linked with the user.""", Extraction.decompose(PostTransactionWhereJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val addWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add where tag @@ -2170,7 +2376,10 @@ Authentication via OAuth is required if the view is not public.""", |OAuth authentication is required since the geo tag is linked with the user.""", Extraction.decompose(PostTransactionWhereJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val updateWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update where tag @@ -2203,7 +2412,10 @@ Authentication via OAuth is required if the view is not public.""", Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the geo tag.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagMetaData)) lazy val deleteWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete where tag @@ -2235,7 +2447,10 @@ Authentication via OAuth is required. The user must either have owner privileges Authentication via OAuth is required if the view is not public.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagTransactions, apiTagCounterparties)) lazy val getCounterpartyForTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get other account of a transaction @@ -2271,7 +2486,10 @@ Authentication via OAuth is required if the view is not public.""", |There are no checks for 'sufficient funds' at the moment, so it is possible to go into unlimited overdraft.""", Extraction.decompose(MakePaymentJson("To BANK_ID", "To ACCOUNT_ID", "12.45")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagPayment)) lazy val makePayment : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: Nil JsonPost json -> _ => { diff --git a/src/main/scala/code/api/v1_3_0/APIMethods130.scala b/src/main/scala/code/api/v1_3_0/APIMethods130.scala index 6ef7e53ad..26f870b69 100644 --- a/src/main/scala/code/api/v1_3_0/APIMethods130.scala +++ b/src/main/scala/code/api/v1_3_0/APIMethods130.scala @@ -39,7 +39,10 @@ trait APIMethods130 { "Returns data about all the physical cards a user has been issued. These could be debit cards, credit cards, etc.", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + Nil) lazy val getCards : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "cards" :: Nil JsonGet _ => { @@ -68,7 +71,10 @@ trait APIMethods130 { "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + Nil) def getCardsForBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 6dcc4652a..06e9f5d5f 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -72,7 +72,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer)) lazy val getCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "customer" :: Nil JsonGet _ => { @@ -101,7 +104,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer)) lazy val getCustomerMessages : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "customer" :: "messages" :: Nil JsonGet _ => { @@ -131,7 +137,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ // We use Extraction.decompose to convert to json Extraction.decompose(AddCustomerMessageJson("message to send", "from department", "from person")), emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer) ) lazy val addCustomerMessage : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -173,7 +182,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |${authenticationRequiredMessage(!getBranchesIsPublic)}""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + true, + false, + List(apiTagBanks) ) lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -215,7 +227,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |${authenticationRequiredMessage(!getAtmsIsPublic)}""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + true, + false, + List(apiTagBanks) ) lazy val getAtms : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -265,7 +280,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |${authenticationRequiredMessage(!getProductsIsPublic)}""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + true, + false, + List(apiTagBanks) ) lazy val getProducts : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -300,7 +318,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer) ) lazy val getCrmEvents : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -334,7 +355,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagPayment)) lazy val getTransactionRequestTypes: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -367,7 +391,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagPayment)) lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => { @@ -410,7 +437,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ ) ), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagPayment)) lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -456,7 +486,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "", Extraction.decompose(ChallengeAnswerJSON("89123812", "123345")), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagPayment)) lazy val answerTransactionRequestChallenge: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -502,7 +535,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), exampleDate, "Single", 1, List(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)), emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer)) lazy val addCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //updates a view on a bank account @@ -573,7 +609,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |_etc_...""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + Nil) } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 9f1333f77..05742025e 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -3,6 +3,8 @@ package code.api.v2_0_0 import java.text.SimpleDateFormat import code.api.util.APIUtil + + import net.liftweb.http.{JsonResponse, Req} import net.liftweb.json.Extraction import net.liftweb.common._ @@ -79,7 +81,10 @@ trait APIMethods200 { |""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagPrivateData, apiTagPublicData)) lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for all banks (private + public) @@ -102,7 +107,10 @@ trait APIMethods200 { |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagAccounts, apiTagPrivateData)) lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for all banks @@ -129,7 +137,10 @@ trait APIMethods200 { |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagPublicData)) lazy val publicAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for all banks @@ -157,7 +168,11 @@ trait APIMethods200 { """, emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagPrivateData, apiTagPublicData) + ) lazy val allAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for a single bank (private + public) @@ -185,7 +200,10 @@ trait APIMethods200 { |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + true, + true, + List(apiTagAccounts, apiTagPrivateData)) def privateAccountsAtOneBankResult (bank: Bank, u: User) = { @@ -230,7 +248,10 @@ trait APIMethods200 { |Authentication via OAuth is not required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagAccounts, apiTagPublicData)) lazy val publicAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for a single bank @@ -257,7 +278,10 @@ trait APIMethods200 { |Authentication is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc)) lazy val getKycDocuments : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "customers" :: customerNumber :: "kyc_documents" :: Nil JsonGet _ => { @@ -287,7 +311,10 @@ trait APIMethods200 { |Authentication is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc)) lazy val getKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "customers" :: customerNumber :: "kyc_media" :: Nil JsonGet _ => { @@ -316,7 +343,10 @@ trait APIMethods200 { |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc)) lazy val getKycChecks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "customers" :: customerNumber :: "kyc_checks" :: Nil JsonGet _ => { @@ -344,7 +374,10 @@ trait APIMethods200 { |Authentication is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc)) lazy val getKycStatuses : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "customers" :: customerNumber :: "kyc_statuses" :: Nil JsonGet _ => { @@ -373,7 +406,10 @@ trait APIMethods200 { |Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, - emptyObjectJson :: Nil) + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc)) lazy val getSocialMediaHandles : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "customers" :: customerNumber :: "social_media_handles" :: Nil JsonGet _ => { @@ -403,7 +439,10 @@ trait APIMethods200 { "", Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc) ) // TODO customerNumber should be in the url but not also in the postedData @@ -444,7 +483,10 @@ trait APIMethods200 { "", Extraction.decompose(KycMediaJSON("73hyfgayt6ywerwerasd", "1239879", "image", "http://www.example.com/id-docs/123/image.png", exampleDate, "wuwjfuha234678", "98FRd987auhf87jab")), emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc) ) lazy val addKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -483,7 +525,10 @@ trait APIMethods200 { "", Extraction.decompose(KycCheckJSON("98FRd987auhf87jab", "1239879", exampleDate, "online_meeting", "67876", "Simon Redfern", true, "")), emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc) ) lazy val addKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -523,7 +568,10 @@ trait APIMethods200 { "", Extraction.decompose(KycStatusJSON("8762893876", true, exampleDate)), emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer, apiTagKyc) ) lazy val addKycStatus : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -558,7 +606,10 @@ trait APIMethods200 { "", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), emptyObjectJson, - emptyObjectJson :: Nil + emptyObjectJson :: Nil, + false, + false, + List(apiTagCustomer) ) lazy val addSocialMediaHandle : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { From 25409faed151e3a8216ee3ea226aedad7a776e90 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 9 Feb 2016 11:37:11 +0100 Subject: [PATCH 339/702] Added props option to make consumer_key non-mandatory --- src/main/scala/code/api/directlogin.scala | 89 ++++++++++++----------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 5663d03a8..216d56de2 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -38,7 +38,7 @@ import net.liftweb.http._ import net.liftweb.http.rest.RestHelper import net.liftweb.json.Extraction import net.liftweb.mapper.By -import net.liftweb.util.Helpers +import net.liftweb.util.{Props, Helpers} import net.liftweb.util.Helpers._ import scala.compat.Platform @@ -87,7 +87,7 @@ object DirectLogin extends RestHelper with Loggable { dlServe { - //Handling get request for an "authorization token" + //Handling get request for a token case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest|GetRequest) => { //Extract the directLogin parameters from the header and test if the request is valid var (httpCode, message, directLoginParameters) = validator("authorizationToken", getHttpMethod) @@ -109,15 +109,9 @@ object DirectLogin extends RestHelper with Loggable { } def getHttpMethod = S.request match { - case Full(s) => { - if (s.post_?) - "POST" - else if (s.get_?) - "GET" - else if (s.put_?) - "PUT" - else - "UNKNOWN" + case Full(s) => s.post_? match { + case true => "POST" + case _ => "GET" } case _ => "ERROR" } @@ -125,11 +119,11 @@ object DirectLogin extends RestHelper with Loggable { //Check if the request (access token or request token) is valid and return a tuple def validator(requestType : String, httpMethod : String) : (Int, String, Map[String,String]) = { //return a Map containing the directLogin parameters : prameter -> value - def getAllParameters : Map[String,String]= { - def toMap(parametersList : String) = { + def getAllParameters: Map[String, String] = { + def toMap(parametersList: String) = { //transform the string "directLogin_prameter="value"" //to a tuple (directLogin_parameter,Decoded(value)) - def dynamicListExtract(input: String) = { + def dynamicListExtract(input: String) = { val directLoginPossibleParameters = List( "consumer_key", @@ -138,25 +132,25 @@ object DirectLogin extends RestHelper with Loggable { "password" ) if (input contains "=") { - val split = input.split("=",2) - val parameterValue = split(1).replace("\"","") + val split = input.split("=", 2) + val parameterValue = split(1).replace("\"", "") //add only OAuth parameters and not empty - if(directLoginPossibleParameters.contains(split(0)) && ! parameterValue.isEmpty) - Some(split(0),parameterValue) // return key , value + if (directLoginPossibleParameters.contains(split(0)) && !parameterValue.isEmpty) + Some(split(0), parameterValue) // return key , value else None } else None } - //we delete the "Oauth" prefix and all the white spaces that may exist in the string - val cleanedParameterList = parametersList.stripPrefix("DirectLogin").replaceAll("\\s","") + //we delete the "DirectLogin" prefix and all the white spaces that may exist in the string + val cleanedParameterList = parametersList.stripPrefix("DirectLogin").replaceAll("\\s", "") val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) params } S.request match { - case Full(a) => a.header("Authorization") match { + case Full(a) => a.header("Authorization") match { case Full(header) => { if (header.contains("DirectLogin")) toMap(header) @@ -170,51 +164,60 @@ object DirectLogin extends RestHelper with Loggable { } - def registeredApplication(consumerKey : String ) : Boolean = { - Consumer.find(By(Consumer.key,consumerKey)) match { + def registeredApplication(consumerKey: String): Boolean = { + Consumer.find(By(Consumer.key, consumerKey)) match { case Full(application) => application.isActive case _ => false } } - def validAccessToken(tokenKey : String) = { - Token.find(By(Token.key, tokenKey),By(Token.tokenType,TokenType.Access)) match { + def validAccessToken(tokenKey: String) = { + Token.find(By(Token.key, tokenKey), By(Token.tokenType, TokenType.Access)) match { case Full(token) => token.isValid - case _ => false + case _ => false } } //@return the missing parameters depending of the request type - def missingDirectLoginParameters(parameters : Map[String, String], requestType : String) : Set[String] = { - //println(parameters.toString) - if(requestType=="authorizationToken") - ("username" :: "password" :: "consumer_key" :: List()).toSet diff parameters.keySet - else if(requestType=="protectedResource") - ("token" :: List()).toSet diff parameters.keySet - else - parameters.keySet + def missingDirectLoginParameters(parameters: Map[String, String], requestType: String): Set[String] = { + requestType match { + case "authorizationToken" => + ("username" :: "password" :: "consumer_key" :: List()).toSet diff parameters.keySet + case "protectedResource" => + ("token" :: List()).toSet diff parameters.keySet + case _ => + parameters.keySet + } } var message = "" - var httpCode : Int = 500 + var httpCode: Int = 500 val parameters = getAllParameters //are all the necessary directLogin parameters present? - val missingParams = missingDirectLoginParameters(parameters,requestType) - if( missingParams.nonEmpty ) - { + val missingParams = missingDirectLoginParameters(parameters, requestType) + if (missingParams.nonEmpty) { message = "the following parameters are missing : " + missingParams.mkString(", ") httpCode = 400 } else if ( - requestType=="protectedResource" && - ! validAccessToken(parameters.get("token").get) - ) - { + requestType == "protectedResource" && + !validAccessToken(parameters.get("token").get) + ) { message = "Invalid or expired access token: " + parameters.get("token").get httpCode = 401 } + //check if the application is registered and active + else if ( + requestType == "authorizationToken" && + Props.getBool("direct_login_consumer_key_mandatory", true) && + !registeredApplication(parameters.get("consumer_key").get)) { + + logger.error("application: " + parameters.get("consumer_key").get + " not found") + message = "Invalid consumer credentials" + httpCode = 401 + } //checking if the signature is correct //else if(! correctSignature(parameters, httpMethod)) //{ @@ -244,7 +247,7 @@ object DirectLogin extends RestHelper with Loggable { import code.model.{Token, TokenType} val token = Token.create token.tokenType(TokenType.Access) - Consumer.find(By(Consumer.key,directLoginParameters.get("consumer_key").get)) match { + Consumer.find(By(Consumer.key, directLoginParameters.get("consumer_key").get)) match { case Full(consumer) => token.consumerId(consumer.id) case _ => None } From 07e6c4c185f05ced8ffc3cc9dc43ee4c77718c30 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 9 Feb 2016 11:45:59 +0100 Subject: [PATCH 340/702] Making apiTag tag one word --- src/main/scala/code/api/util/APIUtil.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index c8ad8aeb1..bd6e5978f 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -275,16 +275,16 @@ object APIUtil extends Loggable { case class ResourceDocTag(tag: String) val apiTagPayment = ResourceDocTag("Payments") - val apiTagApiInfo = ResourceDocTag("API Info") + val apiTagApiInfo = ResourceDocTag("APIInfo") val apiTagBanks = ResourceDocTag("Banks") val apiTagAccounts = ResourceDocTag("Accounts") - val apiTagPublicData = ResourceDocTag("Public Data") - val apiTagPrivateData = ResourceDocTag("Private Data") + val apiTagPublicData = ResourceDocTag("PublicData") + val apiTagPrivateData = ResourceDocTag("PrivateData") val apiTagTransactions = ResourceDocTag("Transactions") val apiTagMetaData = ResourceDocTag("Meta Data") val apiTagViews = ResourceDocTag("Views") val apiTagEntitlements = ResourceDocTag("Entitlements") - val apiTagOwnerRequired = ResourceDocTag("Owner View Required") + val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired") val apiTagCounterparties = ResourceDocTag("Counterparties") val apiTagKyc = ResourceDocTag("KYC") val apiTagCustomer = ResourceDocTag("Customer") From c79c4fbbecb6187adad4e27d94f0295ba7251291 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 9 Feb 2016 14:51:11 +0100 Subject: [PATCH 341/702] Added props option to make consumer_key non-mandatory --- src/main/scala/code/api/directlogin.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 216d56de2..82b61cdf1 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -44,11 +44,10 @@ import net.liftweb.util.Helpers._ import scala.compat.Platform /** -* This object provides the API calls necessary to third party applications -* so they could authenticate their users. +* This object provides the API calls necessary to +* authenticate users using JSON Web Tokens (http://jwt.io). */ - object JSONFactory { case class TokenJSON( token : String ) @@ -73,6 +72,7 @@ object JSONFactory { object DirectLogin extends RestHelper with Loggable { + // Our version of serve def dlServe(handler : PartialFunction[Req, JsonResponse]) : Unit = { val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = { new PartialFunction[Req, () => Box[LiftResponse]] { From 7375d222df49983d03962b26cf1a56d661e28f37 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 9 Feb 2016 17:39:34 +0100 Subject: [PATCH 342/702] sample props: messageQueue.updateBankAccountsTransaction=false --- src/main/resources/props/sample.props.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 986542c41..b7c87f182 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -88,7 +88,7 @@ connection.password=thepassword importer_secret=change_me #set this to true if you want to have the api send a message to the hbci project to refresh transactions for an account -messageQueue.updateBankAccountsTransaction=true +messageQueue.updateBankAccountsTransaction=false #the minimum time between updates in hours messageQueue.updateTransactionsInterval=1 From 08617e36aa42d6d2d1bc218b3a7a10c7ec5ab412 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 10 Feb 2016 10:50:24 +0100 Subject: [PATCH 343/702] Adding is_core, is_psd2, is_obwg, tags to ResourceDocJson --- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index a69714788..abe69c220 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -247,14 +247,18 @@ object JSONFactory1_4_0 { description: String, example_request_body: JValue, success_response_body: JValue, - implemented_by: ImplementedByJson) + implemented_by: ImplementedByJson, + is_core: Boolean, + is_psd2: Boolean, + is_obwg: Boolean, + tags: List[String]) // Creates the json resource_docs case class ResourceDocsJson (resource_docs : List[ResourceDocJson]) - def createResourceDocJson(resourceDoc: ResourceDoc) : ResourceDocJson = { + def createResourceDocJson(rd: ResourceDoc) : ResourceDocJson = { // There are multiple flavours of markdown. For instance, original markdown emphasises underscores (surrounds _ with ()) // But we don't want to have to escape underscores (\_) in our documentation @@ -266,15 +270,19 @@ object JSONFactory1_4_0 { val pegDownProcessor : PegDownProcessor = new PegDownProcessor ResourceDocJson( - operation_id = s"${resourceDoc.apiVersion.toString}-${resourceDoc.apiFunction.toString}", - request_verb = resourceDoc.requestVerb, - request_url = resourceDoc.requestUrl, - summary = resourceDoc.summary, + operation_id = s"${rd.apiVersion.toString}-${rd.apiFunction.toString}", + request_verb = rd.requestVerb, + request_url = rd.requestUrl, + summary = rd.summary, // Strip the margin character (|) and line breaks and convert from markdown to html - description = pegDownProcessor.markdownToHtml(resourceDoc.description.stripMargin).replaceAll("\n", ""), - example_request_body = resourceDoc.exampleRequestBody, - success_response_body = resourceDoc.successResponseBody, - implemented_by = ImplementedByJson(resourceDoc.apiVersion, resourceDoc.apiFunction) + description = pegDownProcessor.markdownToHtml(rd.description.stripMargin).replaceAll("\n", ""), + example_request_body = rd.exampleRequestBody, + success_response_body = rd.successResponseBody, + implemented_by = ImplementedByJson(rd.apiVersion, rd.apiFunction), + is_core = rd.isCore, + is_psd2 = rd.isPSD2, + is_obwg = rd.isCore, // For now, track isCore + tags = rd.tags.map(i => i.tag) ) } From 2cf2c2a74b0d7fa923b578ad5e726596902d6ce7 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 11 Feb 2016 13:08:14 +0100 Subject: [PATCH 344/702] Add first core API call (we assume owner view is requested) --- .../scala/code/api/v2_0_0/APIMethods200.scala | 82 +++++++++++++++++-- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 71 +++++++++++++++- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- 3 files changed, 146 insertions(+), 10 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 05742025e..475928010 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -4,6 +4,12 @@ import java.text.SimpleDateFormat import code.api.util.APIUtil +import code.api.v1_2_1.{ + JSONFactory => JSONFactory121 +} + + + import net.liftweb.http.{JsonResponse, Req} import net.liftweb.json.Extraction @@ -45,9 +51,9 @@ trait APIMethods200 { val views = account.permittedViews(user) val viewsAvailable : List[ViewBasicJSON] = views.map( v => { - JSONFactory.createViewBasicJSON(v) + JSONFactory200.createViewBasicJSON(v) }) - JSONFactory.createAccountBasicJSON(account,viewsAvailable) + JSONFactory200.createAccountBasicJSON(account,viewsAvailable) }) accJson } @@ -291,7 +297,7 @@ trait APIMethods200 { cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycDocuments = KycDocuments.kycDocumentProvider.vend.getKycDocuments(cNumber) - val json = JSONFactory.createKycDocumentsJSON(kycDocuments) + val json = JSONFactory200.createKycDocumentsJSON(kycDocuments) successJsonResponse(Extraction.decompose(json)) } } @@ -324,7 +330,7 @@ trait APIMethods200 { cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycMedias = KycMedias.kycMediaProvider.vend.getKycMedias(cNumber) - val json = JSONFactory.createKycMediasJSON(kycMedias) + val json = JSONFactory200.createKycMediasJSON(kycMedias) successJsonResponse(Extraction.decompose(json)) } } @@ -356,7 +362,7 @@ trait APIMethods200 { cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycChecks = KycChecks.kycCheckProvider.vend.getKycChecks(cNumber) - val json = JSONFactory.createKycChecksJSON(kycChecks) + val json = JSONFactory200.createKycChecksJSON(kycChecks) successJsonResponse(Extraction.decompose(json)) } } @@ -387,7 +393,7 @@ trait APIMethods200 { cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycStatuses = KycStatuses.kycStatusProvider.vend.getKycStatuses(cNumber) - val json = JSONFactory.createKycStatusesJSON(kycStatuses) + val json = JSONFactory200.createKycStatusesJSON(kycStatuses) successJsonResponse(Extraction.decompose(json)) } } @@ -419,7 +425,7 @@ trait APIMethods200 { cNumber <- tryo(customerNumber) ?~! {"Unknown customer"} } yield { val kycSocialMedias = SocialMediaHandle.socialMediaHandleProvider.vend.getSocialMedias(cNumber) - val json = JSONFactory.createSocialMediasJSON(kycSocialMedias) + val json = JSONFactory200.createSocialMediasJSON(kycSocialMedias) successJsonResponse(Extraction.decompose(json)) } } @@ -636,6 +642,68 @@ trait APIMethods200 { } } + resourceDocs += ResourceDoc( + coreAccountById, + apiVersion, + "accountById", + "GET", + "/core/banks/BANK_ID/accounts/ACCOUNT_ID/account", + "Get account by id.", + """Information returned about an account specified by ACCOUNT_ID: + | + |* Number + |* Owners + |* Type + |* Balance + |* IBAN + | + | + |OAuth authentication is required""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + apiTagAccounts :: Nil) + + lazy val coreAccountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get account by id (assume owner view requested) + case "core" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "account" :: Nil JsonGet json => { + + println("in core") + user => + // TODO return specific error if bankId == "BANK_ID" or accountID == "ACCOUNT_ID" + // Should be a generic guard we can use for all calls (also for userId etc.) + for { + account <- BankAccount(bankId, accountId) + availableviews <- Full(account.permittedViews(user)) + //view <- View.fromUrl(viewId, account) + view <- View.fromUrl( ViewId("owner"), account) + moderatedAccount <- account.moderatedBankAccount(view, user) + } yield { + val viewsAvailable = availableviews.map(JSONFactory121.createViewJSON) + val moderatedAccountJson = JSONFactory200.createCoreBankAccountJSON(moderatedAccount, viewsAvailable) + val response = successJsonResponse(Extraction.decompose(moderatedAccountJson)) + response + } + } + } + + + + + + + + + + + + + + + + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 995afbd56..7a159def1 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -40,6 +40,14 @@ import code.socialmedia.SocialMedia import net.liftweb.common.{Box, Full} import code.model._ +// Import explicitly and rename so its clear. +import code.api.v1_2_1.{ + AmountOfMoneyJSON, + UserJSON, + ViewJSON, + JSONFactory => JSONFactory121 +} + @@ -63,6 +71,14 @@ case class AccountBasicJSON( bank_id : String ) + + + + + + + + case class KycDocumentJSON( id: String, customer_number: String, @@ -113,7 +129,7 @@ case class SocialMediaJSON( ) case class SocialMediasJSON(checks: List[SocialMediaJSON]) -object JSONFactory{ +object JSONFactory200{ // New in 2.0.0 @@ -144,6 +160,47 @@ object JSONFactory{ ) } + + + + + + case class ModeratedCoreAccountJSON( + id : String, + label : String, + number : String, + owners : List[UserJSON], + `type` : String, + balance : AmountOfMoneyJSON, + IBAN : String, + swift_bic: String, + bank_id : String + ) + + + + + + def createCoreBankAccountJSON(account : ModeratedBankAccount, viewsAvailable : List[ViewJSON]) : ModeratedCoreAccountJSON = { + val bankName = account.bankName.getOrElse("") + new ModeratedCoreAccountJSON ( + account.accountId.value, + JSONFactory121.stringOptionOrNull(account.label), + JSONFactory121.stringOptionOrNull(account.number), + JSONFactory121.createOwnersJSON(account.owners.getOrElse(Set()), bankName), + JSONFactory121.stringOptionOrNull(account.accountType), + JSONFactory121.createAmountOfMoneyJSON(account.currency.getOrElse(""), account.balance), + JSONFactory121.stringOptionOrNull(account.iban), + JSONFactory121.stringOptionOrNull(account.swift_bic), + stringOrNull(account.bankId.value) + ) + } + + + + + + def createKycDocumentJSON(kycDocument : KycDocument) : KycDocumentJSON = { new KycDocumentJSON( id = kycDocument.idKycDocument, @@ -214,14 +271,24 @@ object JSONFactory{ def createSocialMediasJSON(messages : List[SocialMedia]) : SocialMediasJSON = { SocialMediasJSON(messages.map(createSocialMediaJSON)) } - // From 1.2.1 + // Copied from 1.2.1 (import just this def instead?) def stringOrNull(text : String) = if(text == null || text.isEmpty) null else text + // Copied from 1.2.1 (import just this def instead?) + def stringOptionOrNull(text : Option[String]) = + text match { + case Some(t) => stringOrNull(t) + case _ => null + } + + + + } \ No newline at end of file diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index ea3546b46..bc679b7c0 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -154,7 +154,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.addKycMedia, Implementations2_0_0.addKycStatus, Implementations2_0_0.addKycCheck, - Implementations2_0_0.addSocialMediaHandle + Implementations2_0_0.addSocialMediaHandle, + Implementations2_0_0.coreAccountById ) routes.foreach(route => { From 358682fe83395608640277d79ab3c3479a511b38 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 15 Feb 2016 15:33:40 +0100 Subject: [PATCH 345/702] Replaced regex with JSON parser and assembler in KafkaHelper.scala --- .../code/bankconnectors/KafkaHelper.scala | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaHelper.scala b/src/main/scala/code/bankconnectors/KafkaHelper.scala index 29c9d6983..bbed014f0 100644 --- a/src/main/scala/code/bankconnectors/KafkaHelper.scala +++ b/src/main/scala/code/bankconnectors/KafkaHelper.scala @@ -25,19 +25,12 @@ Berlin 13359, Germany import java.util.{Properties, UUID} -import scala.concurrent.ops._ -import scala.concurrent.duration._ - -import net.liftweb.util.Props - -import kafka.utils.{ZkUtils, ZKStringSerializer} -import org.I0Itec.zkclient.ZkClient -import kafka.consumer.Consumer -import kafka.consumer._ -import kafka.consumer.KafkaStream +import kafka.consumer.{Consumer, _} import kafka.message._ import kafka.producer.{KeyedMessage, Producer, ProducerConfig} - +import kafka.utils.{Json, ZKStringSerializer, ZkUtils} +import net.liftweb.util.Props +import org.I0Itec.zkclient.ZkClient object ZooKeeperUtils { // gets brokers tracked by zookeeper @@ -89,6 +82,7 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")ope val config = new ConsumerConfig(props) config } + def getResponse(reqId: String): List[Map[String, String]] = { // create single stream for topic val topicCountMap = Map(topic -> 1) @@ -107,12 +101,14 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")ope if (key == reqId) { // disconnect from kafka shutdown() - // split result if it contains multiple answers - val msgList = msg.split("\\},\\{") - // match '"":""', with possible space after colon - val p = """"([a-zA-Z0-9_-]*?)":"(.*?)"""".r - val r = (for( m <- msgList) yield (for( p(k, v) <- p.findAllIn(m) ) yield (k -> v)).toMap[String, String]).toList - return r; + // Parse JSON message + val json = Json.parseFull(msg) + val r = json.get match { + case l: List[Map[String,String]] => l.asInstanceOf[List[Map[String, String]]] + case m: Map[String,String] => List(m.asInstanceOf[Map[String, String]]) + case _ => List(Map("error" -> "incorrect JSON format")) + } + return r } } } @@ -129,7 +125,7 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")ope } } -import ZooKeeperUtils._ +import code.bankconnectors.ZooKeeperUtils._ case class KafkaProducer( topic: String = Props.get("kafka.request_topic").openOrThrowException("no kafka.request_topic set"), @@ -172,9 +168,9 @@ case class KafkaProducer( def send(key: String, request: String, arguments: Map[String, String], partition: String = null): Unit = { // create string from named map of arguments - val args = (for ( (k,v) <- arguments ) yield { s""""$k":"$v",""" }).mkString.replaceAll(",$", "") + val args = for ( (k,v) <- arguments ) yield Json.encode(Map(k -> v) ) // create message using request and arguments strings - val message = s"$request:{$args}" + val message = Json.encode(Map(request -> args)) // translate strings to utf8 before sending to kafka send(key.getBytes("UTF8"), message.getBytes("UTF8"), if (partition == null) null else partition.getBytes("UTF8")) } From aac4d9715de05db6fa4c8f5713aca66ce80a8573 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 15 Feb 2016 15:50:13 +0100 Subject: [PATCH 346/702] Added custom CSS for UK --- src/main/webapp/media/css/overrides/uk.css | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/main/webapp/media/css/overrides/uk.css diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css new file mode 100644 index 000000000..533351000 --- /dev/null +++ b/src/main/webapp/media/css/overrides/uk.css @@ -0,0 +1,81 @@ +body { + background-color: white; + font-family: "sans-serif"; + margin: 0; + padding: 0; +} + +h1 { + color: white; + text-align: center; + text-transform: uppercase; +} + + +.container { + background-color: #f4f4f4; + margin: 0 auto; + max-width: 1170px; + overflow: hidden; + padding: 0; +} + +.top { + background-color: #31b8cd; + padding: 40px; +} + +.intro { + margin: 0 20px; + text-align: center; +} + +.content-big { + margin: 30px 200px; + text-align: center; +} + +.buttons { + margin: 40px 0; + text-align: center; +} +.buttons a { + border: 2px solid white; + color: white; + margin-bottom: 10px; + font-weight: bold; + font-size: x-large; + padding: 10px 50px; + text-decoration: none; + text-transform: uppercase; +} + +.content-small { + background-color: white; + margin: 20px 20px 0 20px; + padding: 30px; + text-align: center; +} + +.newsletter { + background-color: black; + margin: 0 20px; + padding: 30px; + text-align: center; +} +.newsletter input { + border: 1px solid black; + border-top-left-radius: 24px; + border-bottom-left-radius: 24px; + padding: 10px 20px; +} +.newsletter button { + border: 1px solid black; + border-left: 0px; + border-top-right-radius: 24px; + border-bottom-right-radius: 24px; + background-color: #50b165; + color: white; + margin-left: -6px; + padding: 10px 30px 10px 10px; +} From 86d0456df9c351dcab6203585b4a51c5432db5b2 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 15 Feb 2016 16:09:19 +0100 Subject: [PATCH 347/702] Removed some page-like bits from UK stylesheet --- src/main/webapp/media/css/overrides/uk.css | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index 533351000..03d2e4925 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -1,10 +1,3 @@ -body { - background-color: white; - font-family: "sans-serif"; - margin: 0; - padding: 0; -} - h1 { color: white; text-align: center; @@ -12,14 +5,6 @@ h1 { } -.container { - background-color: #f4f4f4; - margin: 0 auto; - max-width: 1170px; - overflow: hidden; - padding: 0; -} - .top { background-color: #31b8cd; padding: 40px; From 062684cf3317d06f8c025064484da7a5d17ea7c5 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 15 Feb 2016 16:33:48 +0100 Subject: [PATCH 348/702] Added more adjustments to override CSS for UK --- src/main/webapp/media/css/overrides/uk.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index 03d2e4925..f3a0c80d6 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -1,3 +1,7 @@ +#main-about { + padding-top: 0; +} + h1 { color: white; text-align: center; @@ -49,9 +53,9 @@ h1 { text-align: center; } .newsletter input { - border: 1px solid black; border-top-left-radius: 24px; border-bottom-left-radius: 24px; + height: auto; padding: 10px 20px; } .newsletter button { @@ -60,6 +64,7 @@ h1 { border-top-right-radius: 24px; border-bottom-right-radius: 24px; background-color: #50b165; + font-size: 17px; color: white; margin-left: -6px; padding: 10px 30px 10px 10px; From f08a4a4e9a6a5cb6c6bd4033d9bf4b04bb3c1513 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 15 Feb 2016 16:37:07 +0100 Subject: [PATCH 349/702] Next round of changes to uk.css --- src/main/webapp/media/css/overrides/uk.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index f3a0c80d6..1d05325de 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -4,6 +4,8 @@ h1 { color: white; + font-size: 36px; + margin-bottom: 10px; text-align: center; text-transform: uppercase; } From 135274e656421a852ec46a6f9b2454023b73e421 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 16 Feb 2016 13:40:50 +0100 Subject: [PATCH 350/702] More changes to uk.css --- src/main/webapp/media/css/overrides/uk.css | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index 1d05325de..6f26373e8 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -1,9 +1,6 @@ -#main-about { - padding-top: 0; -} - h1 { color: white; + font-family: "Nunito", "sans-serif"; font-size: 36px; margin-bottom: 10px; text-align: center; @@ -12,21 +9,25 @@ h1 { .top { - background-color: #31b8cd; + background-color: #53C4EF; + font-family: "Nunito", "sans-serif"; padding: 40px; } .intro { + font-family: "Nunito", "sans-serif"; margin: 0 20px; text-align: center; } .content-big { + font-family: "Nunito", "sans-serif"; margin: 30px 200px; text-align: center; } .buttons { + font-family: "Nunito", "sans-serif"; margin: 40px 0; text-align: center; } @@ -34,8 +35,9 @@ h1 { border: 2px solid white; color: white; margin-bottom: 10px; - font-weight: bold; + font-family: "Nunito", "sans-serif"; font-size: x-large; + font-weight: bold; padding: 10px 50px; text-decoration: none; text-transform: uppercase; @@ -43,6 +45,7 @@ h1 { .content-small { background-color: white; + font-family: "Nunito", "sans-serif"; margin: 20px 20px 0 20px; padding: 30px; text-align: center; @@ -50,13 +53,16 @@ h1 { .newsletter { background-color: black; + font-family: "Nunito", "sans-serif"; margin: 0 20px; padding: 30px; text-align: center; } .newsletter input { + border: 1px solid black; border-top-left-radius: 24px; border-bottom-left-radius: 24px; + font-family: "Nunito", "sans-serif"; height: auto; padding: 10px 20px; } @@ -66,8 +72,9 @@ h1 { border-top-right-radius: 24px; border-bottom-right-radius: 24px; background-color: #50b165; - font-size: 17px; color: white; + font-family: "Nunito", "sans-serif"; + font-size: 17px; margin-left: -6px; padding: 10px 30px 10px 10px; } From d626740c3a5d23f36bba86ff0bd452f5a2f37cfe Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 16 Feb 2016 13:58:06 +0100 Subject: [PATCH 351/702] One more change, one more change... to uk.css --- src/main/webapp/media/css/overrides/uk.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index 6f26373e8..e0e91efc4 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -7,6 +7,9 @@ h1 { text-transform: uppercase; } +#main #main-about { + padding-top: 0 !important; +} .top { background-color: #53C4EF; From f95d93e37a2d4cca7b2d8b03277dd920d3eec840 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 16 Feb 2016 16:15:28 +0100 Subject: [PATCH 352/702] Made a round button for uk.css --- src/main/webapp/media/css/overrides/uk.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index e0e91efc4..00d32f797 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -36,6 +36,7 @@ h1 { } .buttons a { border: 2px solid white; + border-radius: 24px; color: white; margin-bottom: 10px; font-family: "Nunito", "sans-serif"; From 0205945233880afb454c6b488142ceeb3ad025c9 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 16 Feb 2016 17:53:56 +0100 Subject: [PATCH 353/702] Ch ch ch changes ... to uk.css --- src/main/webapp/media/css/overrides/uk.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/media/css/overrides/uk.css b/src/main/webapp/media/css/overrides/uk.css index 00d32f797..eec137714 100644 --- a/src/main/webapp/media/css/overrides/uk.css +++ b/src/main/webapp/media/css/overrides/uk.css @@ -25,7 +25,7 @@ h1 { .content-big { font-family: "Nunito", "sans-serif"; - margin: 30px 200px; + margin: 30px 50px; text-align: center; } From e3bead17ce4a9631075dd522de8be1a4c8fdb3aa Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 17 Feb 2016 03:10:58 +0100 Subject: [PATCH 354/702] Adding getCoreTransactionsForBankAccount --- .../resources/props/sample.props.template | 6 +- .../scala/code/api/v1_2_1/APIMethods121.scala | 6 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 94 +++++++++----- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 119 ++++++++++++++++-- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- 5 files changed, 186 insertions(+), 42 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index b7c87f182..efb7d50a0 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -141,4 +141,8 @@ webui_override_style_sheet = ## API Options apiOptions.getBranchesIsPublic = true apiOptions.getAtmsIsPublic = true -apiOptions.getProductsIsPublic = true \ No newline at end of file +apiOptions.getProductsIsPublic = true + +# Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID +# e.g. developers could use /my/accounts as well as /my/banks/BANK_ID/accounts +defaultBank.bank_id=THE_DEFAULT_BANK_ID diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 083b349b9..ee55bd871 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -391,7 +391,7 @@ trait APIMethods121 { emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, - true, + false, true, apiTagAccounts :: Nil) @@ -1820,7 +1820,7 @@ trait APIMethods121 { emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, - true, + false, true, List(apiTagAccounts, apiTagTransactions)) @@ -1854,7 +1854,7 @@ trait APIMethods121 { emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, - true, + false, true, List(apiTagAccounts, apiTagTransactions)) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 475928010..63e0019da 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -4,11 +4,7 @@ import java.text.SimpleDateFormat import code.api.util.APIUtil -import code.api.v1_2_1.{ - JSONFactory => JSONFactory121 -} - - +import code.api.v1_2_1.{JSONFactory => JSONFactory121, APIMethods121} import net.liftweb.http.{JsonResponse, Req} @@ -25,6 +21,7 @@ import code.kycmedias.KycMedias import code.kycstatuses.KycStatuses import code.kycchecks.KycChecks import code.socialmedia.{SocialMediaHandle, SocialMedia} +import net.liftweb.util.Props import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer @@ -41,6 +38,10 @@ trait APIMethods200 { // helper methods begin here + + val defaultBankId = Props.get("defaultBank.bank_id", "DEFAULT_BANK_ID_NOT_SET") + + // New 2.0.0 private def bankAccountBasicListToJson(bankAccounts: List[BankAccount], user : Box[User]): JValue = { Extraction.decompose(bankAccountBasicList(bankAccounts, user)) @@ -105,7 +106,7 @@ trait APIMethods200 { apiVersion, "privateAccountsAllBanks", "GET", - "/accounts/private", + "/my/accounts", "Get private accounts at all banks (Authenticated access).", """Returns the list of accounts containing private views for the user at all banks. |For each account the API returns the ID and the available views. @@ -118,9 +119,12 @@ trait APIMethods200 { true, List(apiTagAccounts, apiTagPrivateData)) + + // TODO This should be more "core" i.e. don't return views. + lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for all banks - case "accounts" :: "private" :: Nil JsonGet json => { + case "my" :: "accounts" :: Nil JsonGet json => { user => for { u <- user ?~ "user not found" @@ -198,7 +202,7 @@ trait APIMethods200 { apiVersion, "privateAccountsAtOneBank", "GET", - "/banks/BANK_ID/accounts/private", + "/my/banks/BANK_ID/accounts", "Get private accounts at one bank (Authenticated access).", """Returns the list of accounts containing private views for the user at BANK_ID. |For each account the API returns the ID and the available views. @@ -217,11 +221,11 @@ trait APIMethods200 { successJsonResponse(bankAccountBasicListToJson(availableAccounts, Full(u))) } - // This contains an approach to surface the same resource via different end point in case of "one" bank. - // The "one" path is experimental and might be removed. + // This contains an approach to surface a resource via different end points in case of a default bank. + // The second path is experimental lazy val privateAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for a single bank - case "banks" :: BankId(bankId) :: "accounts" :: "private" :: Nil JsonGet json => { + case "my" :: "banks" :: BankId(bankId) :: "accounts" :: Nil JsonGet json => { user => for { u <- user ?~ "user not found" @@ -230,11 +234,11 @@ trait APIMethods200 { privateAccountsAtOneBankResult(bank, u) } } - case "one" :: "accounts" :: "private" :: Nil JsonGet json => { + case "accounts" :: Nil JsonGet json => { user => for { u <- user ?~ "user not found" - bank <- Bank(BankId("abc")) + bank <- Bank(BankId(defaultBankId)) } yield { privateAccountsAtOneBankResult(bank, u) } @@ -643,11 +647,11 @@ trait APIMethods200 { } resourceDocs += ResourceDoc( - coreAccountById, + getCoreAccountById, apiVersion, - "accountById", + "coreAccountById", "GET", - "/core/banks/BANK_ID/accounts/ACCOUNT_ID/account", + "/my/banks/BANK_ID/accounts/ACCOUNT_ID/account", "Get account by id.", """Information returned about an account specified by ACCOUNT_ID: | @@ -666,9 +670,9 @@ trait APIMethods200 { true, apiTagAccounts :: Nil) - lazy val coreAccountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + lazy val getCoreAccountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get account by id (assume owner view requested) - case "core" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "account" :: Nil JsonGet json => { + case "my" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "account" :: Nil JsonGet json => { println("in core") user => @@ -677,7 +681,7 @@ trait APIMethods200 { for { account <- BankAccount(bankId, accountId) availableviews <- Full(account.permittedViews(user)) - //view <- View.fromUrl(viewId, account) + // Assume owner view was requested view <- View.fromUrl( ViewId("owner"), account) moderatedAccount <- account.moderatedBankAccount(view, user) } yield { @@ -691,17 +695,51 @@ trait APIMethods200 { + resourceDocs += ResourceDoc( + getCoreTransactionsForBankAccount, + apiVersion, + "getCoreTransactionsForBankAccount", + "GET", + "/my/banks/BANK_ID/accounts/ACCOUNT_ID/transactions", + "Get transactions.", + """Returns transactions list of the account specified by ACCOUNT_ID. + | + |Authentication is required. + | + |Possible custom headers for pagination: + | + |* obp_sort_by=CRITERIA ==> default value: "completed" field + |* obp_sort_direction=ASC/DESC ==> default value: DESC + |* obp_limit=NUMBER ==> default value: 50 + |* obp_offset=NUMBER ==> default value: 0 + |* obp_from_date=DATE => default value: date of the oldest transaction registered (format below) + |* obp_to_date=DATE => default value: date of the newest transaction registered (format below) + | + |**Date format parameter**: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" (2014-07-01T00:00:00.000Z) ==> time zone is UTC.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + List(apiTagAccounts, apiTagTransactions)) + lazy val getCoreTransactionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //get transactions + case "my" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "transactions" :: Nil JsonGet json => { + user => - - - - - - - - - + for { + params <- APIMethods121.getTransactionParams(json) + bankAccount <- BankAccount(bankId, accountId) + // Assume owner view was requested + view <- View.fromUrl( ViewId("owner"), bankAccount) + transactions <- bankAccount.getModeratedTransactions(user, view, params : _*) + } yield { + val json = JSONFactory200.createCoreTransactionsJSON(transactions) + successJsonResponse(Extraction.decompose(json)) + } + } + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 7a159def1..7088ee6b2 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -41,12 +41,7 @@ import net.liftweb.common.{Box, Full} import code.model._ // Import explicitly and rename so its clear. -import code.api.v1_2_1.{ - AmountOfMoneyJSON, - UserJSON, - ViewJSON, - JSONFactory => JSONFactory121 -} +import code.api.v1_2_1.{AccountHolderJSON => AccountHolderJSON121, AmountOfMoneyJSON => AmountOfMoneyJSON121, UserJSON => UserJSON121, ViewJSON => ViewJSON121, ThisAccountJSON => ThisAccountJSON121, OtherAccountJSON => OtherAccountJSON121, TransactionDetailsJSON => TransactionDetailsJSON121, JSONFactory => JSONFactory121, MinimalBankJSON => MinimalBankJSON121} @@ -169,19 +164,125 @@ object JSONFactory200{ id : String, label : String, number : String, - owners : List[UserJSON], + owners : List[UserJSON121], `type` : String, - balance : AmountOfMoneyJSON, + balance : AmountOfMoneyJSON121, IBAN : String, swift_bic: String, bank_id : String ) + //// + + + case class CoreTransactionsJSON( + transactions: List[CoreTransactionJSON] + ) + + case class CoreTransactionJSON( + id : String, + account : ThisAccountJSON121, + counterparty : CoreCounterpartyJSON, + details : CoreTransactionDetailsJSON + ) - def createCoreBankAccountJSON(account : ModeratedBankAccount, viewsAvailable : List[ViewJSON]) : ModeratedCoreAccountJSON = { + case class CoreAccountHolderJSON( + name : String + ) + + + case class CoreCounterpartyJSON( + id : String, + holder : CoreAccountHolderJSON, + number : String, + kind : String, + IBAN : String, + swift_bic: String, + bank : MinimalBankJSON121 + ) + + + + + def createCoreTransactionsJSON(transactions: List[ModeratedTransaction]) : CoreTransactionsJSON = { + new CoreTransactionsJSON(transactions.map(createCoreTransactionJSON)) + } + + case class CoreTransactionDetailsJSON( + `type` : String, + description : String, + posted : Date, + completed : Date, + new_balance : AmountOfMoneyJSON121, + value : AmountOfMoneyJSON121 + ) + + + + def createCoreTransactionDetailsJSON(transaction : ModeratedTransaction) : CoreTransactionDetailsJSON = { + new CoreTransactionDetailsJSON( + `type` = stringOptionOrNull(transaction.transactionType), + description = stringOptionOrNull(transaction.description), + posted = transaction.startDate.getOrElse(null), + completed = transaction.finishDate.getOrElse(null), + new_balance = JSONFactory121.createAmountOfMoneyJSON(transaction.currency, transaction.balance), + value= JSONFactory121.createAmountOfMoneyJSON(transaction.currency, transaction.amount.map(_.toString)) + ) + } + + + def createCoreTransactionJSON(transaction : ModeratedTransaction) : CoreTransactionJSON = { + new CoreTransactionJSON( + id = transaction.id.value, + account = transaction.bankAccount.map(JSONFactory121.createThisAccountJSON).getOrElse(null), + counterparty = transaction.otherBankAccount.map(createCoreCounterparty).getOrElse(null), + details = createCoreTransactionDetailsJSON(transaction) + ) + } + + + + + + case class CounterpartiesJSON( + counterparties : List[CoreCounterpartyJSON] + ) + + + def createCoreCounterparty(bankAccount : ModeratedOtherBankAccount) : CoreCounterpartyJSON = { + new CoreCounterpartyJSON( + id = bankAccount.id, + number = stringOptionOrNull(bankAccount.number), + kind = stringOptionOrNull(bankAccount.kind), + IBAN = stringOptionOrNull(bankAccount.iban), + swift_bic = stringOptionOrNull(bankAccount.swift_bic), + bank = JSONFactory121.createMinimalBankJSON(bankAccount), + holder = createAccountHolderJSON(bankAccount.label.display, bankAccount.isAlias) + ) + } + + + + def createAccountHolderJSON(owner : User, isAlias : Boolean) : CoreAccountHolderJSON = { + // Note we are not using isAlias + new CoreAccountHolderJSON( + name = owner.name + ) + } + + def createAccountHolderJSON(name : String, isAlias : Boolean) : CoreAccountHolderJSON = { + // Note we are not using isAlias + new CoreAccountHolderJSON( + name = name + ) + } + + + + def createCoreBankAccountJSON(account : ModeratedBankAccount, viewsAvailable : List[ViewJSON121]) : ModeratedCoreAccountJSON = { val bankName = account.bankName.getOrElse("") new ModeratedCoreAccountJSON ( account.accountId.value, diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index bc679b7c0..81233ba66 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -155,7 +155,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.addKycStatus, Implementations2_0_0.addKycCheck, Implementations2_0_0.addSocialMediaHandle, - Implementations2_0_0.coreAccountById + Implementations2_0_0.getCoreAccountById, + Implementations2_0_0.getCoreTransactionsForBankAccount ) routes.foreach(route => { From c06e33cd0934667c380c5271fc8666a743535463 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 17 Feb 2016 16:34:19 +0100 Subject: [PATCH 355/702] Adding special instructions to login page. Setting via Props --- .../resources/props/sample.props.template | 4 ++++ src/main/scala/code/snippet/Login.scala | 20 +++++++++++++++++-- .../webapp/templates-hidden/_login_form.html | 10 +++++++++- src/main/webapp/templates-hidden/default.html | 4 ++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index efb7d50a0..05e3a7a31 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -121,6 +121,10 @@ webui_api_explorer_url = http://sofi.openbankproject.com/api-explorer webui_api_documentation_url = https://github.com/OpenBankProject/OBP-API/wiki +# To display a custom message above the username / password box +# We currently use this to display example customer login in sandbox etc. +webui_login_page_special_instructions= + # Link for SDKs webui_sdks_url = https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SDKS diff --git a/src/main/scala/code/snippet/Login.scala b/src/main/scala/code/snippet/Login.scala index b58700eff..2a234543b 100644 --- a/src/main/scala/code/snippet/Login.scala +++ b/src/main/scala/code/snippet/Login.scala @@ -33,9 +33,10 @@ Berlin 13359, Germany package code.snippet import code.model.dataAccess.OBPUser +import net.liftweb.common.Loggable import scala.xml.NodeSeq import net.liftweb.util.Helpers._ -import net.liftweb.util.CssSel +import net.liftweb.util.{Props, CssSel} import net.liftweb.http.S import code.model.dataAccess.Admin import net.liftweb.http.SHtml @@ -70,7 +71,7 @@ class Login { } } } - + def adminLogout : CssSel = { if(Admin.loggedIn_?) { val current = Admin.currentUser @@ -84,4 +85,19 @@ class Login { } } + + // Used to display custom message to users when they login. + // For instance we can use it to display example login on a sandbox + def customiseLogin = { + val specialLoginInstructions = Props.get("webui_login_page_special_instructions", "") + // In case we use Extraction.decompose + implicit val formats = net.liftweb.json.DefaultFormats + "#login_special_instructions" #> specialLoginInstructions + } + + + + + +// End of class } \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/_login_form.html b/src/main/webapp/templates-hidden/_login_form.html index 60b9c1873..59b8c1fe6 100644 --- a/src/main/webapp/templates-hidden/_login_form.html +++ b/src/main/webapp/templates-hidden/_login_form.html @@ -1,4 +1,10 @@ +
    +
    - \ No newline at end of file + + +
    \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index d6492611f..2d0ab85f4 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -1,6 +1,6 @@ diff --git a/src/test/scala/LiftConsole.scala b/src/test/scala/LiftConsole.scala index a4ea1c9ba..f49699015 100755 --- a/src/test/scala/LiftConsole.scala +++ b/src/test/scala/LiftConsole.scala @@ -1,19 +1,28 @@ -/** -Open Bank Project +/** +Open Bank Project - API +Copyright (C) 2011-2016, TESOBE Ltd. -Copyright 2011,2012 TESOBE / Music Pictures Ltd. +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. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +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. -http://www.apache.org/licenses/LICENSE-2.0 +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 Ltd. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. */ import bootstrap.liftweb.Boot import scala.tools.nsc.MainGenericRunner diff --git a/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala b/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala index 02c24e78a..d19261ef4 100644 --- a/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala +++ b/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala @@ -1,21 +1,30 @@ package code.sandbox /** -Open Bank Project +Open Bank Project - API +Copyright (C) 2011-2016, TESOBE Ltd. -Copyright 2011,2016 TESOBE Ltd. +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. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +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. -http://www.apache.org/licenses/LICENSE-2.0 +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 Ltd. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. */ /* diff --git a/src/test/scala/code/sandbox/PostCustomer.scala b/src/test/scala/code/sandbox/PostCustomer.scala index 2bd1b0d46..9e1d63fdf 100644 --- a/src/test/scala/code/sandbox/PostCustomer.scala +++ b/src/test/scala/code/sandbox/PostCustomer.scala @@ -1,21 +1,30 @@ package code.sandbox /** -Open Bank Project +Open Bank Project - API +Copyright (C) 2011-2016, TESOBE Ltd. -Copyright 2011,2016 TESOBE Ltd. +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. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +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. -http://www.apache.org/licenses/LICENSE-2.0 +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 Ltd. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. */ /* From d5ee43250e071b38cd93c87c5ea560121911d338 Mon Sep 17 00:00:00 2001 From: andrisak Date: Tue, 10 May 2016 22:37:46 +0200 Subject: [PATCH 445/702] Fix for issue #16. There is now a check that prevents an empty view id on creation. --- src/main/scala/code/views/MapperViews.scala | 13 +++++++----- src/test/scala/code/api/API121Test.scala | 22 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/views/MapperViews.scala b/src/main/scala/code/views/MapperViews.scala index 79a14137d..f6010aad2 100644 --- a/src/main/scala/code/views/MapperViews.scala +++ b/src/main/scala/code/views/MapperViews.scala @@ -160,16 +160,20 @@ private object MapperViews extends Views with Loggable { } def createView(bankAccount: BankAccount, view: ViewCreationJSON): Box[View] = { + if(view.name.contentEquals("")) { + return Failure("It is not allowed to create a view with an empty name") + } + val newViewPermalink = { - view.name.replaceAllLiterally(" ","").toLowerCase + view.name.replaceAllLiterally(" ", "").toLowerCase } val existing = ViewImpl.count( - By(ViewImpl.permalink_, newViewPermalink) :: + By(ViewImpl.permalink_, newViewPermalink) :: ViewImpl.accountFilter(bankAccount.bankId, bankAccount.accountId): _* - ) == 1 + ) == 1 - if(existing) + if (existing) Failure(s"There is already a view with permalink $newViewPermalink on this bank account") else { val createdView = ViewImpl.create. @@ -181,7 +185,6 @@ private object MapperViews extends Views with Loggable { createdView.setFromViewData(view) Full(createdView.saveMe) } - } def updateView(bankAccount : BankAccount, viewId: ViewId, viewUpdateJson : ViewUpdateData) : Box[View] = { diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index b10372b8b..b7c26f4f8 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -1459,6 +1459,28 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser And("we should get an error message") reply.body.extract[ErrorMessage].error.nonEmpty should equal (true) } + + scenario("we are not allowed to create a view with an empty name") { + Given("We will use an access token") + val bankId = randomBank + val bankAccount : AccountJSON = randomPrivateAccount(bankId) + val viewsBefore = getAccountViews(bankId, bankAccount.id, user1).body.extract[ViewsJSON].views + val viewWithEmptyName = ViewCreationJSON( + name = "", + description = randomString(3), + is_public = true, + which_alias_to_use="alias", + hide_metadata_if_alias_used = false, + allowed_actions = viewFields + ) + + When("the request is sent") + val reply = postView(bankId, bankAccount.id, viewWithEmptyName, user1) + Then("we should get a 400 code") + reply.code should equal (400) + And("we should get an error message") + reply.body.extract[ErrorMessage].error.nonEmpty should equal (true) + } } feature("Update a view on a bank account") { From 8020b5312535a13949689c0c0aa0acccdfa7ac41 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 13 May 2016 18:38:40 +0200 Subject: [PATCH 446/702] Upgraded to scala 2.11 --- README.md | 4 +- pom.xml | 73 +++--- project/build.properties | 2 +- project/build.scala | 20 +- project/plugins.sbt | 17 +- src/main/scala/code/api/directlogin.scala | 2 +- src/main/scala/code/api/util/APIUtil.scala | 151 ++++++------ .../code/bankconnectors/LocalConnector.scala | 32 +-- .../bankconnectors/LocalMappedConnector.scala | 22 +- src/main/scala/code/model/OAuth.scala | 29 +-- .../model/dataAccess/OBPTransaction.scala | 2 +- src/test/scala/code/api/API121Test.scala | 2 +- src/test/scala/code/api/API12Test.scala | 2 +- .../api/LocalMappedConnectorTestSetup.scala | 6 +- .../scala/code/api/PrivateUser2Accounts.scala | 1 - .../scala/code/api/SendServerRequests.scala | 226 +++++++++++++----- src/test/scala/code/api/ServerSetup.scala | 7 +- .../scala/code/api/User1AllPrivileges.scala | 2 - src/test/scala/code/api/directloginTest.scala | 34 +-- src/test/scala/code/api/oauthTest.scala | 25 +- .../code/api/v1_3_0/PhysicalCardsTest.scala | 24 +- src/test/scala/code/api/v1_4_0/AtmsTest.scala | 2 - .../scala/code/api/v1_4_0/ProductsTest.scala | 2 +- .../api/v1_4_0/TransactionRequestsTest.scala | 15 +- .../code/api/v1_4_0/V140ServerSetup.scala | 3 +- .../api/v2_0_0/TransactionRequestsTest.scala | 16 +- .../code/api/v2_0_0/V200ServerSetup.scala | 3 +- .../code/atms/MappedAtmsProviderTest.scala | 2 +- .../BankAccountCreationListenerTest.scala | 2 +- .../BankAccountCreationTest.scala | 2 +- .../branches/MappedBranchesProviderTest.scala | 2 +- .../code/crm/MappedCrmEventProviderTest.scala | 2 +- .../customer/MappedCustomerInfoTest.scala | 2 +- .../code/management/AccountsAPITest.scala | 10 +- .../scala/code/management/ImporterTest.scala | 4 +- src/test/scala/code/metrics/MetricsTest.scala | 2 +- .../products/MappedProductsProviderTest.scala | 2 +- .../sandbox/PostCounterpartyMetadata.scala | 15 +- .../scala/code/sandbox/PostCustomer.scala | 13 +- .../code/sandbox/SandboxDataLoadingTest.scala | 2 +- src/test/scala/code/tesobe/CashAPITest.scala | 11 +- 41 files changed, 431 insertions(+), 364 deletions(-) diff --git a/README.md b/README.md index 707efec92..0598591b7 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ To compile and run jetty, install Maven 3 and execute: * (Alternatively you can do File -> New -> Project from VCS and checkout from github) -* When / if prompted, choose Java 1.7 and Scala 2.10 otherwise keep the defaults. Use the Maven options. Do not change the project name etc. +* When / if prompted, choose Java 1.8 and Scala 2.11 otherwise keep the defaults. Use the Maven options. Do not change the project name etc. -* Navigate to test/scala/code/RunWebApp. You may see a Setup Scala SDK link. Click this and check Scala 2.10.5 or so. +* Navigate to test/scala/code/RunWebApp. You may see a Setup Scala SDK link. Click this and check Scala 2.11.8 or so. * In src/main/resources/props create a test.default.props for tests. Set connector=mapped diff --git a/pom.xml b/pom.xml index 7d259c4d1..15a9e0788 100644 --- a/pom.xml +++ b/pom.xml @@ -11,9 +11,9 @@ Open Bank Project API 2011 - 2.10 - 2.10.5 - 2.6.2 + 2.11 + 2.11.7 + 2.6.3 UTF-8 ${project.build.sourceEncoding} @@ -50,6 +50,11 @@ + + org.scala-lang.modules + scala-xml_${scala.version} + 1.0.5 + net.liftweb lift-mapper_${scala.version} @@ -58,16 +63,16 @@ net.databinder.dispatch dispatch-lift-json_${scala.version} - 0.10.1 + 0.11.3 net.databinder.dispatch dispatch-core_${scala.version} - 0.10.1 + 0.11.3 org.apache.kafka - kafka_2.10 + kafka_${scala.version} 0.8.2.2 @@ -76,44 +81,44 @@ 1.46 - postgresql + org.postgresql postgresql - 8.4-701.jdbc4 + 9.4-1206-jdbc4 com.h2database h2 - 1.2.138 + 1.4.191 runtime javax.servlet - servlet-api - 2.5 + javax.servlet-api + 3.1.0 provided junit junit - 4.9 + 4.12 test org.scalatest scalatest_${scala.version} - 2.0 + 2.2.6 test org.eclipse.jetty jetty-server - 7.6.16.v20140903 + 9.2.15.v20160210 test org.eclipse.jetty jetty-webapp - 7.6.16.v20140903 + 9.2.15.v20160210 test @@ -129,27 +134,32 @@ org.seleniumhq.selenium selenium-java - 2.46.0 + 2.52.0 + + + org.seleniumhq.selenium + selenium-htmlunit-driver + 2.52.0 org.apache.httpcomponents httpclient - 4.5 + 4.5.2 net.sourceforge.htmlunit htmlunit - 2.18 + 2.21 com.rabbitmq amqp-client - 3.2.0 + 3.6.1 net.liftmodules amqp_2.6_${scala.version} - 1.3 + 1.4-SNAPSHOT oauth.signpost @@ -163,7 +173,7 @@ com.jason-goodwin - authentikat-jwt_2.10 + authentikat-jwt_${scala.version} 0.4.1 @@ -175,7 +185,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.9 + ${scala.version} true @@ -184,7 +194,7 @@ org.scalatest scalatest-maven-plugin - 1.0-RC1 + 1.0 ${project.build.directory}/surefire-reports once @@ -205,7 +215,7 @@ org.codehaus.mojo build-helper-maven-plugin - 1.9.1 + 1.10 generate-sources @@ -223,7 +233,7 @@ net.alchim31.maven scala-maven-plugin - 3.2.0 + 3.2.2 ${project.build.sourceEncoding} @@ -242,7 +252,6 @@ - -make:transitive -dependencyfile ${project.build.directory}/.scala_dependencies -Xmax-classfile-name @@ -294,7 +303,7 @@ org.mortbay.jetty maven-jetty-plugin - 6.1.9 + 6.1.26 / 5 @@ -304,7 +313,7 @@ net.alchim31.maven yuicompressor-maven-plugin - 1.3.2 + 1.5.1 @@ -320,7 +329,7 @@ org.apache.maven.plugins maven-idea-plugin - 2.2 + 2.2.1 true @@ -328,7 +337,7 @@ org.apache.maven.plugins maven-eclipse-plugin - 2.7 + 2.10 true @@ -346,7 +355,7 @@ pl.project13.maven git-commit-id-plugin - 2.1.0 + 2.2.1 @@ -368,7 +377,7 @@ net.alchim31.maven scala-maven-plugin - 3.2.0 + 3.2.2 ${project.build.sourceEncoding} diff --git a/project/build.properties b/project/build.properties index b6c3c3270..f4864c146 100755 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ #Project properties -sbt.version=0.11.3 +sbt.version=0.13.9 diff --git a/project/build.scala b/project/build.scala index 9dc1a79d4..910a377ab 100755 --- a/project/build.scala +++ b/project/build.scala @@ -1,29 +1,21 @@ - - import sbt._ import Keys._ -import com.github.siasia._ -import PluginKeys._ -import WebPlugin._ -import WebappPlugin._ +import com.earldouglas.xwp._ + object LiftProjectBuild extends Build { override lazy val settings = super.settings ++ buildSettings - + lazy val buildSettings = Seq( organization := pom.groupId, version := pom.version ) - def yourWebSettings = webSettings ++ Seq( - // If you are use jrebel - scanDirectories in Compile := Nil - ) - lazy val opanBank = Project( pom.artifactId, base = file("."), - settings = defaultSettings ++ yourWebSettings ++ pom.settings) + settings = defaultSettings ++ pom.settings) + .enablePlugins(JettyPlugin) object pom { @@ -66,7 +58,7 @@ object LiftProjectBuild extends Build { populateProps((rep \ "url").text) at populateProps((rep \ "url").text) } - lazy val pomScalaVersion = (pom \ "properties" \ "scala.version").text + lazy val pomScalaVersion = (pom \ "properties" \ "scala.compiler").text lazy val artifactId = (pom \ "artifactId").text lazy val groupId = (pom \ "groupId").text diff --git a/project/plugins.sbt b/project/plugins.sbt index 259302b12..3642ac1be 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,14 +7,12 @@ resolvers += Classpaths.typesafeResolver //xsbt-web-plugin -resolvers += "Web plugin repo" at "http://siasia.github.com/maven2" -libraryDependencies <+= sbtVersion(v => v match { -case "0.11.0" => "com.github.siasia" %% "xsbt-web-plugin" % "0.11.0-0.2.8" -case "0.11.1" => "com.github.siasia" %% "xsbt-web-plugin" % "0.11.1-0.2.10" -case "0.11.2" => "com.github.siasia" %% "xsbt-web-plugin" % "0.11.2-0.2.11" -case "0.11.3" => "com.github.siasia" %% "xsbt-web-plugin" % "0.11.3-0.2.11.1" -}) +addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0") + +//resolvers += "Web plugin repo" at "http://siasia.github.com/maven2" + +//libraryDependencies <+= sbtVersion(v => "com.github.siasia" % "xsbt-web-plugin" % (v+"-0.2.11")) //sbteclipse resolvers += { @@ -23,9 +21,10 @@ resolvers += { Resolver.url("Typesafe Repository", typesafeRepoUrl)(pattern) } -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.2") //sbt-idea resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/" -addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.0.0") +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 046931e97..d261a3737 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -102,7 +102,7 @@ object DirectLogin extends RestHelper with Loggable { httpCode = 401 } else { val claims = Map("" -> "") - val (token, secret) = generateTokenAndSecret(claims) + val (token:String, secret:String) = generateTokenAndSecret(claims) //Save the token that we have generated if (saveAuthorizationToken(directLoginParameters, token, secret, userId)) { diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 946615bd4..655c75305 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -1,60 +1,54 @@ /** -Open Bank Project - API -Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd - -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 / Music Pictures Ltd -Osloerstrasse 16/17 -Berlin 13359, Germany - - This product includes software developed at - TESOBE (http://www.tesobe.com/) - by - Simon Redfern : simon AT tesobe DOT com - Stefan Bethge : stefan AT tesobe DOT com - Everett Sochowski : everett AT tesobe DOT com - Ayoub Benali: ayoub AT tesobe DOT com - + * Open Bank Project - API + * Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd + ** + *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 / Music Pictures Ltd +*Osloerstrasse 16/17 +*Berlin 13359, Germany + ** + *This product includes software developed at + *TESOBE (http://www.tesobe.com/) + * by + *Simon Redfern : simon AT tesobe DOT com + *Stefan Bethge : stefan AT tesobe DOT com + *Everett Sochowski : everett AT tesobe DOT com + *Ayoub Benali: ayoub AT tesobe DOT com + * */ package code.api.util -import code.api.util.APIUtil.ApiLink + +import code.api.Constant._ import code.api.v1_2.ErrorMessage import code.metrics.APIMetrics import code.model._ +import dispatch.url import net.liftweb.common.{Box, Full, Loggable} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.js.JsExp -import net.liftweb.http.{JsonResponse, Req, S} -import net.liftweb.json.Extraction -import net.liftweb.json._ +import net.liftweb.http.{CurrentReq, JsonResponse, Req, S} import net.liftweb.json.JsonAST.JValue +import net.liftweb.json.{Extraction, parse} import net.liftweb.util.Helpers._ -import net.liftweb.util.Props - -import scala.collection.JavaConversions.asScalaSet +import net.liftweb.util.{Props, Helpers, SecurityHelpers} import scala.collection.mutable.ArrayBuffer - -import net.liftweb.http.CurrentReq -import code.api.Constant._ - - - +import scala.collection.JavaConverters._ object ErrorMessages { @@ -188,8 +182,17 @@ object APIUtil extends Loggable { import org.apache.http.protocol.HTTP.UTF_8 import scala.collection.Map - import scala.collection.immutable.{Map => IMap, TreeMap} - import scala.collection.mutable.Set + import scala.collection.immutable.{TreeMap, Map => IMap} + + case class ReqData ( + url: String, + method: String, + body: String, + body_encoding: String, + headers: Map[String, String], + query_params: Map[String,String], + form_params: Map[String,String] + ) case class Consumer(key: String, secret: String) case class Token(value: String, secret: String) @@ -225,7 +228,7 @@ object APIUtil extends Loggable { val sig = { val mac = crypto.Mac.getInstance(SHA1) mac.init(key) - Helpers.base64Encode(mac.doFinal(bytes(message))) + base64Encode(mac.doFinal(bytes(message))) } oauth_params + ("oauth_signature" -> sig) } @@ -256,7 +259,7 @@ object APIUtil extends Loggable { def decode_% (s: String) = java.net.URLDecoder.decode(s, org.apache.http.protocol.HTTP.UTF_8) class RequestSigner(rb: Request) { - private val r = rb.build() + private val r = rb.toRequest @deprecated("use <@ (consumer, callback) to pass the callback in the header for a request-token request") def <@ (consumer: Consumer): Request = sign(consumer, None, None, None) /** sign a request with a callback, e.g. a request-token request */ @@ -275,43 +278,43 @@ object APIUtil extends Loggable { /** Sign request by reading Post (<<) and query string parameters */ private def sign(consumer: Consumer, token: Option[Token], verifier: Option[String], callback: Option[String]) = { - val split_decode: (String => IMap[String, String]) = { - case null => IMap.empty - case query => - if(query.isEmpty) - IMap.empty - else - IMap.empty ++ query.trim.split('&').map { nvp => - nvp.split("=").map(decode_%) match { - case Array(name) => name -> "" - case Array(name, value) => name -> value - } - } - } + val oauth_url = r.getUrl.split('?')(0) - val query_params = split_decode(tryo{r.getUrl.split('?')(1)}getOrElse("")) - val params = r.getParams - val keys : Set[String] = tryo{asScalaSet(params.keySet)}.getOrElse(Set()) - val form_params = keys.map{ k => - (k -> params.get(k)) - } + val query_params = r.getQueryParams.asScala.groupBy(_.getName).mapValues(_.map(_.getValue)).map { + case (k, v) => k -> v.toString + } + val form_params = r.getFormParams.asScala.groupBy(_.getName).mapValues(_.map(_.getValue)).map { + case (k, v) => k -> v.toString + } + val body_encoding = r.getBodyEncoding + var body = new String() + if (r.getByteData != null ) + body = new String(r.getByteData) val oauth_params = OAuth.sign(r.getMethod, oauth_url, query_params ++ form_params, consumer, token, verifier, callback) - def addHeader(rb : Request, values: Map[String, String]) : Request = { - values.map{ case (k,v) => - rb.setHeader(k, v) - } + def createRequest( reqData: ReqData ): Request = { + val rb = url(reqData.url) + .setMethod(reqData.method) + .setBodyEncoding(reqData.body_encoding) + .setBody(reqData.body) <:< reqData.headers + if (reqData.query_params.nonEmpty) + rb < ("OAuth " + oauth_params.map { - case (k, v) => (encode_%(k)) + "=\"%s\"".format(encode_%(v)) - }.mkString(",") )) - ) + case (k, v) => encode_%(k) + "=\"%s\"".format(encode_%(v.toString)) + }.mkString(",") )), + query_params, + form_params + )) } } } diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 57dc51632..dbf5a08ad 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -1,26 +1,28 @@ package code.bankconnectors import java.text.SimpleDateFormat -import java.util.{Date, UUID, TimeZone} +import java.util.{Date, TimeZone, UUID} + import code.management.ImporterAPI.ImporterTransaction -import code.tesobe.CashTransaction -import code.transactionrequests.TransactionRequests.{TransactionRequestCharge, TransactionRequestChallenge, TransactionRequest, TransactionRequestBody} -import code.util.Helper -import net.liftweb.common.{Failure, Box, Loggable, Full} -import net.liftweb.json.Extraction -import net.liftweb.json.JsonAST.JValue -import scala.concurrent.ops.spawn +import code.metadata.counterparties.{Counterparties, Metadata, MongoCounterparties} import code.model._ import code.model.dataAccess._ +import code.tesobe.CashTransaction +import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} +import code.util.Helper +import com.mongodb.QueryBuilder +import com.tesobe.model.UpdateBankAccount +import net.liftweb.common.{Box, Failure, Full, Loggable} +import net.liftweb.json.Extraction +import net.liftweb.json.JsonAST.JValue import net.liftweb.mapper.By import net.liftweb.mongodb.BsonDSL._ -import org.bson.types.ObjectId import net.liftweb.util.Helpers._ import net.liftweb.util.Props -import com.mongodb.QueryBuilder -import code.metadata.counterparties.{Counterparties, MongoCounterparties, Metadata} -import com.tesobe.model.{CreateBankAccount, UpdateBankAccount} +import org.bson.types.ObjectId +import scala.concurrent._ +import scala.concurrent.ExecutionContext.Implicits.global import scala.math.BigDecimal.RoundingMode private object LocalConnector extends Connector with Loggable { @@ -246,7 +248,7 @@ private object LocalConnector extends Connector with Loggable { } envJson = - ("obp_transaction" -> + "obp_transaction" -> ("this_account" -> ("holder" -> account.owners.headOption.map(_.name).getOrElse("")) ~ //TODO: this is rather fragile... ("number" -> account.number) ~ @@ -277,7 +279,7 @@ private object LocalConnector extends Connector with Loggable { ("amount" -> (oldBalance + amount).toString)) ~ ("value" -> ("currency" -> account.currency) ~ - ("amount" -> amount.toString)))) + ("amount" -> amount.toString))) saved <- saveAndUpdateAccountBalance(envJson, account) } yield { saved @@ -294,7 +296,7 @@ private object LocalConnector extends Connector with Loggable { */ private def updateAccountTransactions(bank: HostedBank, account: Account): Unit = { - spawn{ + Future { val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) val outDatedTransactions = now after time(account.accountLastUpdate.get.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1))) if(outDatedTransactions && useMessageQueue) { diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index db69f747e..4a8fd58c4 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -1,31 +1,29 @@ package code.bankconnectors -import java.util.{Calendar, UUID, Date} +import java.util.{Date, UUID} import code.fx.fx +import code.management.ImporterAPI.ImporterTransaction import code.metadata.comments.MappedComment import code.metadata.counterparties.Counterparties import code.metadata.narrative.MappedNarrative import code.metadata.tags.MappedTag import code.metadata.transactionimages.MappedTransactionImage import code.metadata.wheretags.MappedWhereTag -import code.model.dataAccess.ViewImpl -import code.model.dataAccess.ViewPrivileges - import code.model._ -import code.model.dataAccess.{UpdatesRequestSender, MappedBankAccount, MappedAccountHolder, MappedBank} +import code.model.dataAccess._ import code.tesobe.CashTransaction -import code.management.ImporterAPI.ImporterTransaction -import code.transactionrequests.{TransactionRequests, MappedTransactionRequest} -import code.transactionrequests.TransactionRequests.{TransactionRequestCharge, TransactionRequestChallenge, TransactionRequest, TransactionRequestBody} +import code.transactionrequests.MappedTransactionRequest +import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} import code.util.Helper import com.tesobe.model.UpdateBankAccount -import net.liftweb.common.{Loggable, Full, Box, Failure} +import net.liftweb.common.{Box, Failure, Full, Loggable} import net.liftweb.mapper._ import net.liftweb.util.Helpers._ -import net.liftweb.util.{False, Props} +import net.liftweb.util.Props -import scala.concurrent.ops._ +import scala.concurrent._ +import scala.concurrent.ExecutionContext.Implicits.global object LocalMappedConnector extends Connector with Loggable { @@ -94,7 +92,7 @@ object LocalMappedConnector extends Connector with Loggable { bank <- getMappedBank(bankId) account <- getBankAccountType(bankId, accountId) } { - spawn{ + Future{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) val outDatedTransactions = Box!!account.accountLastUpdate.get match { case Full(l) => now after time(l.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1))) diff --git a/src/main/scala/code/model/OAuth.scala b/src/main/scala/code/model/OAuth.scala index 51422d2a8..8c287b163 100644 --- a/src/main/scala/code/model/OAuth.scala +++ b/src/main/scala/code/model/OAuth.scala @@ -30,21 +30,22 @@ Berlin 13359, Germany */ package code.model -import net.liftweb.mapper._ +import net.liftweb._ +import net.liftweb.mapper.{LongKeyedMetaMapper, _} import net.liftweb.util.FieldError import net.liftweb.common.{Full,Failure,Box,Empty} -import net.liftweb.util.Helpers +import net.liftweb.util.{Helpers, SecurityHelpers} import Helpers.now import code.model.dataAccess.APIUser import net.liftweb.http.S import net.liftweb.util.Helpers._ -object AppType extends Enumeration("web", "mobile"){ +object AppType extends Enumeration { type AppType = Value val Web, Mobile = Value } -object TokenType extends Enumeration("request", "access"){ +object TokenType extends Enumeration { type TokenType=Value val Request, Access = Value } @@ -109,7 +110,7 @@ class Consumer extends LongKeyedMapper[Consumer] with CreatedUpdated{ object Consumer extends Consumer with LongKeyedMetaMapper[Consumer] with CRUDify[Long, Consumer]{ //list all path : /admin/consumer/list override def calcPrefix = List("admin",_dbTableNameLC) - + //obscure primary key to avoid revealing information about, e.g. how many consumers are registered // (by incrementing ids until receiving a "log in first" page instead of 404) val obfuscator = new KeyObfuscator() @@ -122,23 +123,23 @@ object Consumer extends Consumer with LongKeyedMetaMapper[Consumer] with CRUDify //override it to list the newest ones first override def findForListParams: List[QueryParam[Consumer]] = List(OrderBy(primaryKeyField, Descending)) - + //We won't display all the fields when we are listing Consumers (to save screen space) override def fieldsForList: List[FieldPointerType] = List(id, name, appType, description, developerEmail, createdAt) - + override def fieldOrder = List(name, appType, description, developerEmail) - + //show more than the default of 20 override def rowsPerPage = 100 - + //counts the number of different unique email addresses val numUniqueEmailsQuery = s"SELECT COUNT(DISTINCT ${Consumer.developerEmail.dbColumnName}) FROM ${Consumer.dbName};" - + val numUniqueAppNames = s"SELECT COUNT(DISTINCT ${Consumer.name.dbColumnName}) FROM ${Consumer.dbName};" - + private val recordsWithUniqueEmails = tryo {Consumer.countByInsecureSql(numUniqueEmailsQuery, IHaveValidatedThisSQL("everett", "2014-04-29")) } private val recordsWithUniqueAppNames = tryo {Consumer.countByInsecureSql(numUniqueAppNames, IHaveValidatedThisSQL("everett", "2014-04-29"))} - + //overridden to display extra stats above the table override def _showAllTemplate = @@ -216,7 +217,7 @@ class Token extends LongKeyedMapper[Token]{ def gernerateVerifier : String = if (verifier.isEmpty){ def fiveRandomNumbers() : String = { - def r() = Helpers.randomInt(9).toString //from zero to 9 + def r() = randomInt(9).toString //from zero to 9 (1 to 5).map(x => r()).foldLeft("")(_ + _) } val generatedVerifier = fiveRandomNumbers() @@ -237,7 +238,7 @@ class Token extends LongKeyedMapper[Token]{ def generateThirdPartyApplicationSecret: String = { if(thirdPartyApplicationSecret isEmpty){ - def r() = Helpers.randomInt(9).toString //from zero to 9 + def r() = randomInt(9).toString //from zero to 9 val generatedSecret = (1 to 10).map(x => r()).foldLeft("")(_ + _) thirdPartyApplicationSecret(generatedSecret).save generatedSecret diff --git a/src/main/scala/code/model/dataAccess/OBPTransaction.scala b/src/main/scala/code/model/dataAccess/OBPTransaction.scala index 12dfec31e..70a3acd17 100644 --- a/src/main/scala/code/model/dataAccess/OBPTransaction.scala +++ b/src/main/scala/code/model/dataAccess/OBPTransaction.scala @@ -243,7 +243,7 @@ class OBPTransaction private() extends BsonRecord[OBPTransaction]{ details.get.validate ++ super.validate - @deprecated(Helper.deprecatedJsonGenerationMessage) + @deprecated(Helper.deprecatedJsonGenerationMessage, null) def whenAddedJson(envelopeId : String) : JObject = { JObject(List(JField("obp_transaction_uuid", JString(envelopeId)), JField("this_account", this_account.get.whenAddedJson), diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index b10372b8b..71605685f 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -46,7 +46,7 @@ import code.model.{Consumer => OBPConsumer, Token => OBPToken, _} import APIUtil.OAuth._ import code.views.Views import net.liftweb.json.JsonAST.JString -import code.api.test.APIResponse +import code.api.APIResponse import net.liftweb.util.Props class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser2Accounts { diff --git a/src/test/scala/code/api/API12Test.scala b/src/test/scala/code/api/API12Test.scala index 46ae8cb90..1d498148b 100644 --- a/src/test/scala/code/api/API12Test.scala +++ b/src/test/scala/code/api/API12Test.scala @@ -42,7 +42,7 @@ import _root_.net.liftweb.json.Serialization.write import _root_.net.liftweb.json.JsonAST.{JObject} import net.liftweb.json.JsonDSL._ import scala.util.Random._ -import code.api.test.{APIResponse} +import code.api.APIResponse import code.model.{Consumer => OBPConsumer, Token => OBPToken, _} import APIUtil.OAuth._ import code.views.Views diff --git a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala index 09827fc8e..ad950b3cc 100644 --- a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala @@ -1,10 +1,10 @@ package code.api -import java.sql.SQLException import java.util.Date + import bootstrap.liftweb.ToSchemify -import code.model.dataAccess._ import code.model._ +import code.model.dataAccess._ import net.liftweb.mapper.MetaMapper import net.liftweb.util.Helpers._ @@ -79,7 +79,7 @@ trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermis .counterpartyIban(randomString(5)) .counterpartyNationalId(randomString(5)) .saveMe - .toTransaction.get + .toTransaction.orNull } override protected def wipeTestData() = { diff --git a/src/test/scala/code/api/PrivateUser2Accounts.scala b/src/test/scala/code/api/PrivateUser2Accounts.scala index 02ada81c0..e0427a7c3 100644 --- a/src/test/scala/code/api/PrivateUser2Accounts.scala +++ b/src/test/scala/code/api/PrivateUser2Accounts.scala @@ -1,6 +1,5 @@ package code.api -import code.api.test.{ServerSetupWithTestData, ServerSetup} import code.bankconnectors.Connector import code.model.{AccountId, User} import net.liftweb.util.Helpers._ diff --git a/src/test/scala/code/api/SendServerRequests.scala b/src/test/scala/code/api/SendServerRequests.scala index abdb5a09d..bb0179384 100644 --- a/src/test/scala/code/api/SendServerRequests.scala +++ b/src/test/scala/code/api/SendServerRequests.scala @@ -1,41 +1,49 @@ /** -Open Bank Project - API -Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd + * Open Bank Project - API + * Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd + ** + *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 / Music Pictures Ltd +*Osloerstrasse 16/17 +*Berlin 13359, Germany + ** + *This product includes software developed at + *TESOBE (http://www.tesobe.com/) + * by + *Simon Redfern : simon AT tesobe DOT com + *Stefan Bethge : stefan AT tesobe DOT com + *Everett Sochowski : everett AT tesobe DOT com + *Ayoub Benali: ayoub AT tesobe DOT com + * + */ +package code.api -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 / Music Pictures Ltd -Osloerstrasse 16/17 -Berlin 13359, Germany - - This product includes software developed at - TESOBE (http://www.tesobe.com/) - by - Simon Redfern : simon AT tesobe DOT com - Stefan Bethge : stefan AT tesobe DOT com - Everett Sochowski : everett AT tesobe DOT com - Ayoub Benali: ayoub AT tesobe DOT com - - */ -package code.api.test +import code.api.util.APIUtil.OAuth +import code.model.{Consumer, Token} +import dispatch.Defaults._ +import dispatch._ import net.liftweb.common.Full import net.liftweb.json.JsonAST.JValue import net.liftweb.json._ +import net.liftweb.mapper._ import net.liftweb.util.Helpers._ -import dispatch._, Defaults._ + +import scala.collection.JavaConverters._ +import scala.collection.immutable.TreeMap import scala.concurrent.Await import scala.concurrent.duration.Duration @@ -43,7 +51,120 @@ case class APIResponse(code: Int, body: JValue) trait SendServerRequests { + case class ReqData ( + url: String, + method: String, + body: String, + body_encoding: String, + headers: Map[String, String], + query_params: Map[String,String], + form_params: Map[String,String] + ) + + def encode_% (s: String) = java.net.URLEncoder.encode(s, org.apache.http.protocol.HTTP.UTF_8) + + def decode_% (s: String) = java.net.URLDecoder.decode(s, org.apache.http.protocol.HTTP.UTF_8) + + //normalize to OAuth percent encoding + def %% (str: String): String = { + val remaps = ("+", "%20") :: ("%7E", "~") :: ("*", "%2A") :: Nil + (encode_%(str) /: remaps) { case (str, (a, b)) => str.replace(a,b) } + } + def %% (s: Seq[String]): String = s map %% mkString "&" + def %% (t: (String, Any)): (String, String) = (%%(t._1), %%(t._2.toString)) + + def getOAuthParameters(headers: Map[String,String]) : Map[String,String]= { + //Convert the string containing the list of OAuth parameters to a Map + def toMap(parametersList : String) = { + //transform the string "oauth_prameter="value"" + //to a tuple (oauth_parameter,Decoded(value)) + def dynamicListExtract(input: String) = { + val oauthPossibleParameters = + List( + "oauth_consumer_key", + "oauth_nonce", + "oauth_signature_method", + "oauth_timestamp", + "oauth_version", + "oauth_signature", + "oauth_callback", + "oauth_token", + "oauth_verifier" + ) + if (input contains "=") { + val split = input.split("=",2) + val parameterValue = split(1).replace("\"","") + //add only OAuth parameters and not empty + if(oauthPossibleParameters.contains(split(0)) && ! parameterValue.isEmpty) + Some(split(0),parameterValue) // return key , value + else + None + } + else + None + } + //we delete the "Oauth" prefix and all the white spaces that may exist in the string + val cleanedParameterList = parametersList.stripPrefix("OAuth").replaceAll("\\s","") + Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) + } + toMap(headers("Authorization")) + } + + def createRequest( reqData: ReqData ): Req = { + val rb = url(reqData.url) + .setMethod(reqData.method) + .setBodyEncoding(reqData.body_encoding) + .setBody(reqData.body) <:< reqData.headers + if (reqData.query_params.nonEmpty) + rb < c.secret + case _ => "" + } + } + + def getTokenSecret(token : String ) : String = { + Token.find(By(Token.key, token)) match { + case Full(t) => t.secret + case _ => "" + } + } + + def extractParamsAndHeaders(req: Req, body: String, encoding: String, extra_headers:Map[String,String] = Map.empty): ReqData= { + val r = req.toRequest + val query_params:Map[String,String] = r.getQueryParams.asScala.map(qp => qp.getName -> qp.getValue).toMap[String,String] + val form_params: Map[String,String] = r.getFormParams.asScala.map( fp => fp.getName -> fp.getValue).toMap[String,String] + var headers:Map[String,String] = r.getHeaders.entrySet.asScala.map (h => h.getKey -> h.getValue.get(0)).toMap[String,String] + val url:String = r.getUrl + val method:String = r.getMethod + + if (headers.isDefinedAt("Authorization") && headers("Authorization").contains("OAuth")) { + val oauth_params = getOAuthParameters(headers) + val consumer_secret = getConsumerSecret(oauth_params("oauth_consumer_key")) + val token_secret = getTokenSecret(oauth_params("oauth_token")) + val new_oauth_params = OAuth.sign( + method, + url, + query_params ++ form_params, // ++ extra_headers, + OAuth.Consumer(oauth_params("oauth_consumer_key"), consumer_secret), + Option(OAuth.Token(oauth_params.getOrElse("oauth_token", ""), token_secret)), + oauth_params.get("verifier"), + oauth_params.get("callback")) + val new_oauth_headers = (new TreeMap[String, String] ++ (new_oauth_params map %%) + ) map { case (k, v) => k + """="""" + v + """"""" } mkString "," + headers = Map("Authorization" -> ("OAuth " + new_oauth_headers)) + } + + ReqData(url, method, body, encoding, headers ++ extra_headers, query_params, form_params) + } + + private def getAPIResponse(req : Req) : APIResponse = { + //println("<<<<<<< " + req.toRequest.toString) Await.result( for(response <- Http(req > as.Response(p => p))) yield @@ -59,39 +180,29 @@ trait SendServerRequests { } /** - this method does a POST request given a URL, a JSON + *this method does a POST request given a URL, a JSON */ def makePostRequest(req: Req, json: String = ""): APIResponse = { - req.addHeader("Content-Type", "application/json") - req.addHeader("Accept", "application/json") - req.setBody(json) - req.setBodyEncoding("UTF-8") - val jsonReq = (req).POST + val extra_headers = Map( "Content-Type" -> "application/json", + "Accept" -> "application/json") + val reqData = extractParamsAndHeaders(req.POST, json, "UTF-8", extra_headers) + val jsonReq = createRequest(reqData) getAPIResponse(jsonReq) } // Accepts an additional option header Map def makePostRequestAdditionalHeader(req: Req, json: String = "", params: List[(String, String)] = Nil): APIResponse = { - req.addHeader("Content-Type", "application/json") - req.addHeader("Accept", "application/json") - req.setBody(json) - req.setBodyEncoding("UTF-8") - val jsonReq = req.POST - params.foreach{ - headerAndValue => { - jsonReq.addHeader(headerAndValue._1, headerAndValue._2) - } - } + val extra_headers = Map( "Content-Type" -> "application/json", + "Accept" -> "application/json") ++ params + val reqData = extractParamsAndHeaders(req.POST, json, "UTF-8", extra_headers) + val jsonReq = createRequest(reqData) getAPIResponse(jsonReq) } - - def makePutRequest(req: Req, json: String = "") : APIResponse = { - req.addHeader("Content-Type", "application/json") - req.setBody(json) - req.setBodyEncoding("UTF-8") - val jsonReq = (req).PUT + val extra_headers = Map("Content-Type" -> "application/json") + val reqData = extractParamsAndHeaders(req.PUT, json, "UTF-8", extra_headers) + val jsonReq = createRequest(reqData) getAPIResponse(jsonReq) } @@ -99,12 +210,9 @@ trait SendServerRequests { * this method does a GET request given a URL */ def makeGetRequest(req: Req, params: List[(String, String)] = Nil) : APIResponse = { - val jsonReq = req.GET - params.foreach{ - headerAndValue => { - jsonReq.addHeader(headerAndValue._1, headerAndValue._2) - } - } + val extra_headers = Map.empty ++ params + val reqData = extractParamsAndHeaders(req.GET, "", "", extra_headers) + val jsonReq = createRequest(reqData) getAPIResponse(jsonReq) } diff --git a/src/test/scala/code/api/ServerSetup.scala b/src/test/scala/code/api/ServerSetup.scala index 990e0fac1..b1a7122f4 100644 --- a/src/test/scala/code/api/ServerSetup.scala +++ b/src/test/scala/code/api/ServerSetup.scala @@ -30,14 +30,13 @@ Berlin 13359, Germany */ -package code.api.test +package code.api import code.TestServer -import code.api.{DefaultConnectorTestSetup, TestConnectorSetup, LocalConnectorTestSetup} -import org.scalatest._ import dispatch._ -import net.liftweb.json.{ShortTypeHints, DefaultFormats, Serialization, NoTypeHints} import net.liftweb.common._ +import net.liftweb.json.{DefaultFormats, ShortTypeHints} +import org.scalatest._ trait ServerSetup extends FeatureSpec with SendServerRequests with BeforeAndAfterEach with GivenWhenThen diff --git a/src/test/scala/code/api/User1AllPrivileges.scala b/src/test/scala/code/api/User1AllPrivileges.scala index fc90f4911..7900ead26 100644 --- a/src/test/scala/code/api/User1AllPrivileges.scala +++ b/src/test/scala/code/api/User1AllPrivileges.scala @@ -1,7 +1,5 @@ package code.api -import code.api.test.{ServerSetupWithTestData} - //a trait that grants obpuser1 from DefaultUsers access to all views before each test trait User1AllPrivileges extends ServerSetupWithTestData { self : DefaultUsers => diff --git a/src/test/scala/code/api/directloginTest.scala b/src/test/scala/code/api/directloginTest.scala index 773c377f1..b911f0344 100644 --- a/src/test/scala/code/api/directloginTest.scala +++ b/src/test/scala/code/api/directloginTest.scala @@ -1,33 +1,25 @@ package code.api -import code.api.test.{APIResponse, ServerSetup} import code.api.util.ErrorMessages import code.model.dataAccess.OBPUser import code.model.{Consumer => OBPConsumer, Token => OBPToken} -import dispatch._ import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString} import net.liftweb.mapper.By import net.liftweb.util.Helpers._ +import org.scalatest.BeforeAndAfter -class directloginTest extends ServerSetup { +class directloginTest extends ServerSetup with BeforeAndAfter{ val KEY = randomString(40).toLowerCase val SECRET = randomString(40).toLowerCase val EMAIL = randomString(10).toLowerCase + "@example.com" val PASSWORD = randomString(20) - def setupUserAndConsumer = { - if (OBPConsumer.find(By(OBPConsumer.key, KEY)).isEmpty) - OBPConsumer.create. - name("test application"). - isActive(true). - key(KEY). - secret(SECRET). - saveMe - + //def setupUserAndConsumer = { + before { if (OBPUser.find(By(OBPUser.email, EMAIL)).isEmpty) OBPUser.create. email(EMAIL). @@ -36,6 +28,14 @@ class directloginTest extends ServerSetup { firstName(randomString(10)). lastName(randomString(10)). saveMe + + if (OBPConsumer.find(By(OBPConsumer.key, KEY)).isEmpty) + OBPConsumer.create. + name("test application"). + isActive(true). + key(KEY). + secret(SECRET). + saveMe } val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") @@ -60,7 +60,7 @@ class directloginTest extends ServerSetup { feature("DirectLogin") { scenario("Invalid auth header") { - setupUserAndConsumer + //setupUserAndConsumer Given("the app we are testing is registered and active") Then("We should be able to find it") @@ -78,7 +78,7 @@ class directloginTest extends ServerSetup { scenario("Invalid credentials") { - setupUserAndConsumer + //setupUserAndConsumer Given("the app we are testing is registered and active") Then("We should be able to find it") @@ -95,7 +95,7 @@ class directloginTest extends ServerSetup { scenario("Missing DirecLogin header") { - setupUserAndConsumer + //setupUserAndConsumer Given("the app we are testing is registered and active") Then("We should be able to find it") @@ -112,7 +112,7 @@ class directloginTest extends ServerSetup { scenario("Login without consumer key") { - setupUserAndConsumer + //setupUserAndConsumer Given("the app we are testing is registered and active") Then("We should be able to find it") @@ -129,7 +129,7 @@ class directloginTest extends ServerSetup { scenario("Login with correct everything!") { - setupUserAndConsumer + //setupUserAndConsumer Given("the app we are testing is registered and active") Then("We should be able to find it") diff --git a/src/test/scala/code/api/oauthTest.scala b/src/test/scala/code/api/oauthTest.scala index 42bdd05c1..8c5578c43 100644 --- a/src/test/scala/code/api/oauthTest.scala +++ b/src/test/scala/code/api/oauthTest.scala @@ -32,22 +32,19 @@ Berlin 13359, Germany package code.api -import code.api.util.APIUtil -import net.liftweb.util.Props -import org.scalatest._ -import dispatch._, Defaults._ -import net.liftweb.util.Helpers._ -import net.liftweb.http.S -import net.liftweb.common.{Box, Loggable} -import code.api.test.{ServerSetup, APIResponse} +import code.api.util.APIUtil.OAuth._ import code.model.dataAccess.OBPUser import code.model.{Consumer => OBPConsumer, Token => OBPToken} -import code.model.TokenType._ +import dispatch.Defaults._ +import dispatch._ +import net.liftweb.common.{Box, Loggable} +import net.liftweb.util.Helpers._ +import net.liftweb.util.Props +import org.scalatest._ import org.scalatest.selenium._ -import org.openqa.selenium.WebDriver -import scala.concurrent.duration._ + import scala.concurrent.Await -import APIUtil.OAuth._ +import scala.concurrent.duration._ case class OAuthResponse( code: Int, @@ -142,13 +139,13 @@ class OAuthTest extends ServerSetup { def getVerifier(requestToken: String, userName: String, password: String): Box[String] = { val b = Browser() - val loginPage = (oauthRequest / "authorize" < OBPConsumer, Token => OBPToken} import java.util.Date -import APIUtil.OAuth._ + +import code.api.util.APIUtil.OAuth._ +import code.api.{DefaultUsers, ServerSetup} +import code.bankconnectors.{Connector, OBPQueryParam} +import code.management.ImporterAPI.ImporterTransaction +import code.model.{PhysicalCard, Consumer => OBPConsumer, Token => OBPToken, _} +import code.tesobe.CashTransaction +import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} +import net.liftweb.common.{Box, Empty, Failure, Loggable} class PhysicalCardsTest extends ServerSetup with DefaultUsers { diff --git a/src/test/scala/code/api/v1_4_0/AtmsTest.scala b/src/test/scala/code/api/v1_4_0/AtmsTest.scala index 0ab1c2a12..c8f05ca9c 100644 --- a/src/test/scala/code/api/v1_4_0/AtmsTest.scala +++ b/src/test/scala/code/api/v1_4_0/AtmsTest.scala @@ -1,7 +1,6 @@ package code.api.v1_4_0 import code.api.DefaultUsers -import code.api.test.ServerSetup import code.api.v1_4_0.JSONFactory1_4_0.{AtmJson, AtmsJson} import code.api.util.APIUtil.OAuth._ @@ -9,7 +8,6 @@ import code.atms.Atms.{Atm, AtmId} import code.atms.{Atms, AtmsProvider} import code.common.{Address, License, Location, Meta} import code.model.BankId -import dispatch._ class AtmsTest extends V140ServerSetup with DefaultUsers { diff --git a/src/test/scala/code/api/v1_4_0/ProductsTest.scala b/src/test/scala/code/api/v1_4_0/ProductsTest.scala index 9287aa6b6..827929fec 100644 --- a/src/test/scala/code/api/v1_4_0/ProductsTest.scala +++ b/src/test/scala/code/api/v1_4_0/ProductsTest.scala @@ -1,7 +1,7 @@ package code.api.v1_4_0 import code.api.DefaultUsers -import code.api.test.ServerSetup +import code.api.ServerSetup import code.api.util.APIUtil.OAuth._ import code.api.v1_4_0.JSONFactory1_4_0.{ProductJson, ProductsJson} import code.common.{License, Meta} diff --git a/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala index 13330d1ee..82fbbefa9 100644 --- a/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v1_4_0/TransactionRequestsTest.scala @@ -1,21 +1,16 @@ package code.api.v1_4_0 -import code.api.DefaultUsers -import code.api.test.{APIResponse, ServerSetupWithTestData, ServerSetup} -import code.api.util.APIUtil.OAuth.{Token, Consumer} -import code.api.v1_2_1.{TransactionsJSON, TransactionJSON, MakePaymentJson, AmountOfMoneyJSON} +import code.api.{DefaultUsers, ServerSetupWithTestData} +import code.api.util.APIUtil.OAuth._ +import code.api.v1_2_1.AmountOfMoneyJSON import code.api.v1_4_0.JSONFactory1_4_0._ import code.bankconnectors.Connector -import code.model.{TransactionRequestId, AccountId, BankAccount} +import code.model.{AccountId, BankAccount, TransactionRequestId} import code.transactionrequests.TransactionRequests -import code.api.util.APIUtil.OAuth._ -import dispatch._ import net.liftweb.json.JsonAST.JString -import net.liftweb.json._ +import net.liftweb.json.Serialization.write import net.liftweb.util.Props import org.scalatest.Tag -import java.util.Calendar -import net.liftweb.json.Serialization.{read, write} class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers with V140ServerSetup { diff --git a/src/test/scala/code/api/v1_4_0/V140ServerSetup.scala b/src/test/scala/code/api/v1_4_0/V140ServerSetup.scala index 215208dac..698ef58a5 100644 --- a/src/test/scala/code/api/v1_4_0/V140ServerSetup.scala +++ b/src/test/scala/code/api/v1_4_0/V140ServerSetup.scala @@ -1,7 +1,6 @@ package code.api.v1_4_0 -import code.api.test.{ServerSetupWithTestData, ServerSetup} -import dispatch._ +import code.api.ServerSetupWithTestData trait V140ServerSetup extends ServerSetupWithTestData { diff --git a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala index f6129f4e4..40033ace3 100644 --- a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala @@ -1,24 +1,12 @@ package code.api.v2_0_0 -import java.util.Date - -import code.api.DefaultUsers -import code.api.test.ServerSetupWithTestData +import code.api.{DefaultUsers, ServerSetupWithTestData} import code.api.util.APIUtil.OAuth._ import code.api.v1_2_1.AmountOfMoneyJSON -import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, ChallengeJSON, TransactionRequestAccountJSON} - -import code.api.v2_0_0.TransactionRequestBodyJSON - - -import code.api.v2_0_0.TransactionRequestJSON - - +import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJSON} import code.bankconnectors.Connector import code.fx.fx import code.model.{AccountId, BankAccount, TransactionRequestId} -import code.transactionrequests.TransactionRequests -import dispatch._ import net.liftweb.json.JsonAST.JString import net.liftweb.json.Serialization.write import net.liftweb.util.Props diff --git a/src/test/scala/code/api/v2_0_0/V200ServerSetup.scala b/src/test/scala/code/api/v2_0_0/V200ServerSetup.scala index 2bc047c1e..203cfaf75 100644 --- a/src/test/scala/code/api/v2_0_0/V200ServerSetup.scala +++ b/src/test/scala/code/api/v2_0_0/V200ServerSetup.scala @@ -1,7 +1,6 @@ package code.api.v2_0_0 -import code.api.test.{ServerSetupWithTestData, ServerSetup} -import dispatch._ +import code.api.ServerSetupWithTestData trait V200ServerSetup extends ServerSetupWithTestData { diff --git a/src/test/scala/code/atms/MappedAtmsProviderTest.scala b/src/test/scala/code/atms/MappedAtmsProviderTest.scala index eb9238bd2..b3a24e9f4 100644 --- a/src/test/scala/code/atms/MappedAtmsProviderTest.scala +++ b/src/test/scala/code/atms/MappedAtmsProviderTest.scala @@ -1,6 +1,6 @@ package code.atms -import code.api.test.ServerSetup +import code.api.ServerSetup import code.atms.Atms.Atm import code.model.BankId import net.liftweb.mapper.By diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala index 7796f4c30..f4da4b412 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala @@ -1,7 +1,7 @@ package code.bankaccountcreation import code.api.DefaultConnectorTestSetup -import code.api.test.ServerSetup +import code.api.ServerSetup import code.model.{User, BankId} import code.views.Views import net.liftweb.common.Full diff --git a/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala b/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala index 6baae2db4..4850f3ad0 100644 --- a/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala +++ b/src/test/scala/code/bankaccountcreation/BankAccountCreationTest.scala @@ -1,6 +1,6 @@ package code.bankaccountcreation -import code.api.test.ServerSetup +import code.api.ServerSetup import code.api.{DefaultConnectorTestSetup, DefaultUsers} import code.bankconnectors.Connector import code.model.{AccountId, BankId} diff --git a/src/test/scala/code/branches/MappedBranchesProviderTest.scala b/src/test/scala/code/branches/MappedBranchesProviderTest.scala index 6c2486bc7..f8802a315 100644 --- a/src/test/scala/code/branches/MappedBranchesProviderTest.scala +++ b/src/test/scala/code/branches/MappedBranchesProviderTest.scala @@ -1,6 +1,6 @@ package code.branches -import code.api.test.ServerSetup +import code.api.ServerSetup import code.model.BankId import code.branches.Branches.BranchId import net.liftweb.mapper.By diff --git a/src/test/scala/code/crm/MappedCrmEventProviderTest.scala b/src/test/scala/code/crm/MappedCrmEventProviderTest.scala index c44d021e1..b0ea614c3 100644 --- a/src/test/scala/code/crm/MappedCrmEventProviderTest.scala +++ b/src/test/scala/code/crm/MappedCrmEventProviderTest.scala @@ -3,7 +3,7 @@ package code.crm import java.util.Date import code.api.DefaultUsers -import code.api.test.ServerSetup +import code.api.ServerSetup import code.model.BankId import net.liftweb.mapper.By diff --git a/src/test/scala/code/customer/MappedCustomerInfoTest.scala b/src/test/scala/code/customer/MappedCustomerInfoTest.scala index 403970407..d4b6624f2 100644 --- a/src/test/scala/code/customer/MappedCustomerInfoTest.scala +++ b/src/test/scala/code/customer/MappedCustomerInfoTest.scala @@ -3,7 +3,7 @@ package code.customer import java.util.Date import code.api.DefaultUsers -import code.api.test.ServerSetup +import code.api.ServerSetup import code.model.BankId import net.liftweb.mapper.By diff --git a/src/test/scala/code/management/AccountsAPITest.scala b/src/test/scala/code/management/AccountsAPITest.scala index c08a79959..e30d00dc3 100644 --- a/src/test/scala/code/management/AccountsAPITest.scala +++ b/src/test/scala/code/management/AccountsAPITest.scala @@ -1,12 +1,10 @@ package code.management -import code.api.util.APIUtil.OAuth._ -import code.api.v1_2_1.{AccountsJSON, API1_2_1Test} -import code.api.{PrivateUser2Accounts, DefaultUsers, User1AllPrivileges} -import code.api.test.APIResponse -import code.model.{BankId, AccountId} -import dispatch._ +import code.api._ +import code.api.v1_2_1._ import code.bankconnectors.Connector +import code.model.{AccountId, BankId} +import code.api.util.APIUtil.OAuth.{Consumer, Token, _} import net.liftweb.common.Empty import org.scalatest.Tag diff --git a/src/test/scala/code/management/ImporterTest.scala b/src/test/scala/code/management/ImporterTest.scala index 76ebac962..19b51aead 100644 --- a/src/test/scala/code/management/ImporterTest.scala +++ b/src/test/scala/code/management/ImporterTest.scala @@ -3,11 +3,9 @@ package code.management import java.text.SimpleDateFormat import java.util.TimeZone -import code.api.DefaultConnectorTestSetup -import code.api.test.{APIResponse, ServerSetup} +import code.api.{APIResponse, DefaultConnectorTestSetup, ServerSetup} import code.bankconnectors.Connector import code.model.{AccountId, Transaction} -import dispatch._ import net.liftweb.common.Loggable import net.liftweb.util.Props import net.liftweb.util.TimeHelpers._ diff --git a/src/test/scala/code/metrics/MetricsTest.scala b/src/test/scala/code/metrics/MetricsTest.scala index d729379a3..e18160568 100644 --- a/src/test/scala/code/metrics/MetricsTest.scala +++ b/src/test/scala/code/metrics/MetricsTest.scala @@ -3,7 +3,7 @@ package code.metrics import java.text.SimpleDateFormat import java.util.Date -import code.api.test.ServerSetup +import code.api.ServerSetup /* diff --git a/src/test/scala/code/products/MappedProductsProviderTest.scala b/src/test/scala/code/products/MappedProductsProviderTest.scala index 4901188ec..8fa38f089 100644 --- a/src/test/scala/code/products/MappedProductsProviderTest.scala +++ b/src/test/scala/code/products/MappedProductsProviderTest.scala @@ -1,6 +1,6 @@ package code.products -import code.api.test.ServerSetup +import code.api.ServerSetup import code.products.Products.Product import code.model.BankId import code.util.DefaultStringField diff --git a/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala b/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala index 02c24e78a..0c080cd09 100644 --- a/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala +++ b/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala @@ -31,17 +31,14 @@ limitations under the License. * into your props file. * */ +import code.api.ObpJson.{BarebonesAccountsJson, _} +import code.api.{SendServerRequests, _} +import net.liftweb.common.Full +import net.liftweb.json.JsonDSL._ +import net.liftweb.json._ + import scala.collection.mutable.ListBuffer import scala.io.Source -import dispatch._ -import net.liftweb.json._ -import net.liftweb.json.JsonDSL._ -import net.liftweb.common.Full -import code.api.test.SendServerRequests -import code.api.ObpJson._ -import code.api.util.APIUtil._ -import code.api._ -import code.api.ObpJson.BarebonesAccountsJson case class CounterpartyJSONRecord(name: String, category: String, superCategory: String, logoUrl: String, homePageUrl: String, region: String) case class UserJSONRecord(email: String, password: String, display_name: String) diff --git a/src/test/scala/code/sandbox/PostCustomer.scala b/src/test/scala/code/sandbox/PostCustomer.scala index 2bd1b0d46..fc04849f1 100644 --- a/src/test/scala/code/sandbox/PostCustomer.scala +++ b/src/test/scala/code/sandbox/PostCustomer.scala @@ -33,17 +33,14 @@ limitations under the License. import java.util.Date +import code.api.ObpJson._ +import code.api.{SendServerRequests, _} +import net.liftweb.common.{Box, Empty, Full} import net.liftweb.http.RequestVar +import net.liftweb.json._ import scala.collection.mutable.ListBuffer import scala.io.Source -import dispatch._ -import net.liftweb.json._ -import net.liftweb.json.JsonDSL._ -import net.liftweb.common.{Empty, Box, Full} -import code.api.test.SendServerRequests -import code.api.ObpJson._ -import code.api._ case class CustomerFullJson(customer_number : String, legal_name : String, @@ -170,7 +167,7 @@ object PostCustomer extends SendServerRequests { println(s"we got customer that matches ") - customer.map(c => { + customer.foreach(c => { println (s"email is ${c.email} has ${c.dependants} dependants born on ${c.dob_of_dependants.map(d => s"${d}")} ") // We are able to post this (no need to convert to string explicitly) diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 4fd795b2a..5990517d3 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -36,7 +36,7 @@ import java.util.Date import bootstrap.liftweb.ToSchemify import code.TestServer -import code.api.test.{SendServerRequests, APIResponse} +import code.api.{SendServerRequests, APIResponse} import code.api.v1_2_1.APIMethods121 import code.atms.Atms import code.atms.Atms.{Atm, AtmId, countOfAtms} diff --git a/src/test/scala/code/tesobe/CashAPITest.scala b/src/test/scala/code/tesobe/CashAPITest.scala index 62838849e..1f299bab5 100644 --- a/src/test/scala/code/tesobe/CashAPITest.scala +++ b/src/test/scala/code/tesobe/CashAPITest.scala @@ -1,14 +1,13 @@ package code.tesobe -import java.util.{UUID, Date} -import net.liftweb.json.Serialization.write -import code.api.DefaultConnectorTestSetup -import code.api.test.{APIResponse, ServerSetup} +import java.util.{Date, UUID} + +import code.api.{APIResponse, DefaultConnectorTestSetup, ServerSetup} import code.bankconnectors.Connector import code.model.AccountId -import net.liftweb.common.{Full, Loggable} +import net.liftweb.common.Loggable +import net.liftweb.json.Serialization.write import net.liftweb.util.Props -import dispatch._ /** * The cash api isn't very well designed and is used only for internal projects. If we want to From d67d2689823af8efd54be42178bedbdce5bec307 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 14 May 2016 05:06:15 +0200 Subject: [PATCH 447/702] Refactor: Renamed Kafka*Import case classes to KafkaInbound* --- .../bankconnectors/KafkaMappedConnector.scala | 213 ++++++++++-------- .../scala/code/model/dataAccess/OBPUser.scala | 4 +- 2 files changed, 119 insertions(+), 98 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index d4cd86258..63b8275d0 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -33,15 +33,15 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable type AccountType = KafkaBankAccount val cacheTTL = Props.get("cache.ttl.base", "30").toInt - val cachedUser = TTLCache[KafkaValidatedUserImport](cacheTTL) - val cachedBank = TTLCache[KafkaBankImport](cacheTTL) - val cachedAccount = TTLCache[KafkaAccountImport](cacheTTL) - val cachedBanks = TTLCache[List[KafkaBankImport]](cacheTTL) - val cachedAccounts = TTLCache[List[KafkaAccountImport]](cacheTTL) - val cachedPublicAccounts = TTLCache[List[KafkaAccountImport]](cacheTTL) - val cachedUserAccounts = TTLCache[List[KafkaAccountImport]](cacheTTL) + val cachedUser = TTLCache[KafkaInboundValidatedUser](cacheTTL) + val cachedBank = TTLCache[KafkaInboundBank](cacheTTL) + val cachedAccount = TTLCache[KafkaInboundAccount](cacheTTL) + val cachedBanks = TTLCache[List[KafkaInboundBank]](cacheTTL) + val cachedAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) + val cachedPublicAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) + val cachedUserAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) - def getUser( username: String, password: String ): Box[KafkaUserImport] = { + def getUser( username: String, password: String ): Box[KafkaInboundUser] = { // Generate random uuid to be used as request-respose match id val reqId: String = UUID.randomUUID().toString // Send request to Kafka, marked with reqId @@ -51,17 +51,17 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable implicit val formats = net.liftweb.json.DefaultFormats val r = { - cachedUser.getOrElseUpdate( argList.toString, () => process(reqId, "getUser", argList).extract[KafkaValidatedUserImport]) + cachedUser.getOrElseUpdate( argList.toString, () => process(reqId, "getUser", argList).extract[KafkaInboundValidatedUser]) } val recDisplayName = r.display_name val recEmail = r.email if (recEmail == username.toLowerCase && recEmail != "Not Found") { if (recDisplayName == "") { - val user = new KafkaUserImport( recEmail, password, recEmail) + val user = new KafkaInboundUser( recEmail, password, recEmail) Full(user) } else { - val user = new KafkaUserImport(recEmail, password, recDisplayName) + val user = new KafkaInboundUser(recEmail, password, recDisplayName) Full(user) } } else { @@ -70,7 +70,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } - def setAccountOwner(owner : String, account: KafkaAccountImport) : Unit = { + def setAccountOwner(owner : String, account: KafkaInboundAccount) : Unit = { val apiUserOwner = APIUser.findAll.find(user => owner == user.emailAddress) apiUserOwner match { case Some(o) => { @@ -95,7 +95,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats val rList = { - cachedUserAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaAccountImport]]) + cachedUserAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) } val res = { for (r <- rList) yield { @@ -117,7 +117,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val argList = Map("username" -> user.email.get ) implicit val formats = net.liftweb.json.DefaultFormats val rList = { - cachedPublicAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getPublicAccounts", argList).extract[List[KafkaAccountImport]]) + cachedPublicAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getPublicAccounts", argList).extract[List[KafkaInboundAccount]]) } val res = { for (r <- rList) yield { @@ -129,7 +129,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable res } - def createSaveableViews(acc : KafkaAccountImport) : List[Saveable[ViewType]] = { + def createSaveableViews(acc : KafkaInboundAccount) : List[Saveable[ViewType]] = { val bankId = BankId(acc.bank) val accountId = AccountId(acc.id) @@ -156,11 +156,11 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val reqId: String = UUID.randomUUID().toString // Create empty argument list val argList = Map( "username" -> OBPUser.getCurrentUserUsername ) - // Send request to Kafka, marked with reqId + // Send request to Kafka, marked with reqId // so we can fetch the corresponding response implicit val formats = net.liftweb.json.DefaultFormats val rList = { - cachedBanks.getOrElseUpdate( argList.toString, () => process(reqId, "getBanks", argList).extract[List[KafkaBankImport]]) + cachedBanks.getOrElseUpdate( argList.toString, () => process(reqId, "getBanks", argList).extract[List[KafkaInboundBank]]) } // Loop through list of responses and create entry for each val res = { for ( r <- rList ) yield { @@ -180,17 +180,17 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Create argument list val argList = Map( "bankId" -> id.toString, "username" -> OBPUser.getCurrentUserUsername ) - // Send request to Kafka, marked with reqId + // Send request to Kafka, marked with reqId // so we can fetch the corresponding response implicit val formats = net.liftweb.json.DefaultFormats val r = { - cachedBank.getOrElseUpdate( argList.toString, () => process(reqId, "getBank", argList).extract[KafkaBankImport]) + cachedBank.getOrElseUpdate( argList.toString, () => process(reqId, "getBank", argList).extract[KafkaInboundBank]) } // Return result Full(new KafkaBank(r)) } - // Gets transaction identified by bankid, accountid and transactionId + // Gets transaction identified by bankid, accountid and transactionId def getTransaction(bankId: BankId, accountID: AccountId, transactionId: TransactionId): Box[Transaction] = { // Generate random uuid to be used as request-respose match id val reqId: String = UUID.randomUUID().toString @@ -202,8 +202,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable "transactionId" -> transactionId.toString ) // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats - val r = process(reqId, "getTransaction", argList).extract[KafkaTransactionImport] - // If empty result from Kafka return empty data + val r = process(reqId, "getTransaction", argList).extract[KafkaInboundTransaction] + // If empty result from Kafka return empty data if (r.id == "") { return Failure(null) } @@ -233,8 +233,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable "accountId" -> accountID.toString, "queryParams" -> queryParams.toString ) implicit val formats = net.liftweb.json.DefaultFormats - val rList = process(reqId, "getTransactions", argList).extract[List[KafkaTransactionImport]] - // Return blank if empty + val rList = process(reqId, "getTransactions", argList).extract[List[KafkaInboundTransaction]] + // Return blank if empty if (rList.head.id == "") { return Failure(null) } @@ -259,7 +259,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats val r = { - cachedAccount.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccount", argList).extract[KafkaAccountImport]) + cachedAccount.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccount", argList).extract[KafkaInboundAccount]) } val res = new KafkaBankAccount(r) Full(res) @@ -276,7 +276,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats val r = { - cachedAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccounts", argList).extract[List[KafkaAccountImport]]) + cachedAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccounts", argList).extract[List[KafkaInboundAccount]]) } val res = r.map ( t => new KafkaBankAccount(t) ) res @@ -293,7 +293,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats val r = { - cachedAccount.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccount", argList).extract[KafkaAccountImport]) + cachedAccount.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccount", argList).extract[KafkaInboundAccount]) } val res = new KafkaBankAccount(r) Full(res) @@ -864,7 +864,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Helper for creating a transaction - def createNewTransaction(r: KafkaTransactionImport) = { + def createNewTransaction(r: KafkaInboundTransaction) = { var datePosted: Date = null if (r.details.posted != null && r.details.posted.matches("^[0-9]{8}$")) datePosted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.posted) @@ -897,7 +897,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } - case class KafkaBank(r: KafkaBankImport) extends Bank { + case class KafkaBank(r: KafkaInboundBank) extends Bank { def fullName = r.full_name def shortName = r.short_name def logoUrl = r.logo @@ -924,7 +924,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable ) } - case class KafkaBankAccount(r: KafkaAccountImport) extends BankAccount { + case class KafkaBankAccount(r: KafkaInboundAccount) extends BankAccount { def uuid : String = r.id def accountId : AccountId = AccountId(r.id) def accountType : String = r.`type` @@ -941,7 +941,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } - case class KafkaBankImport( + case class KafkaInboundBank( id : String, short_name : String, full_name : String, @@ -949,31 +949,52 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable website : String) - // Branches to be imported must match this pattern - case class KafkaBranchImport( - id : String, - bank_id: String, - name : String, - address : KafkaAddressImport, - location : KafkaLocationImport, - meta : KafkaMetaImport, - lobby : Option[KafkaLobbyImport], - driveUp : Option[KafkaDriveUpImport]) + /** Bank Branches + * + * @param id Uniquely identifies the Branch within the Bank. SHOULD be url friendly (no spaces etc.) Used in URLs + * @param bank_id MUST match bank_id in Banks + * @param name Informal name for the Branch + * @param address Address + * @param location Geolocation + * @param meta Meta information including the license this information is published under + * @param lobby Info about when the lobby doors are open + * @param driveUp Info about when automated facilities are open e.g. cash point machine + */ + case class KafkaInboundBranch( + id : String, + bank_id: String, + name : String, + address : KafkaInboundAddress, + location : KafkaInboundLocation, + meta : KafkaInboundMeta, + lobby : Option[KafkaInboundLobby], + driveUp : Option[KafkaInboundDriveUp]) - case class KafkaLicenseImport( + case class KafkaInboundLicense( id : String, name : String) - case class KafkaMetaImport( - license : KafkaLicenseImport) + case class KafkaInboundMeta( + license : KafkaInboundLicense) - case class KafkaLobbyImport( + case class KafkaInboundLobby( hours : String) - case class KafkaDriveUpImport( + case class KafkaInboundDriveUp( hours : String) - case class KafkaAddressImport( + /** + * + * @param line_1 Line 1 of Address + * @param line_2 Line 2 of Address + * @param line_3 Line 3 of Address + * @param city City + * @param county County i.e. Division of State + * @param state State i.e. Division of Country + * @param post_code Post Code or Zip Code + * @param country_code 2 letter country code: ISO 3166-1 alpha-2 + */ + case class KafkaInboundAddress( line_1 : String, line_2 : String, line_3 : String, @@ -983,51 +1004,51 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable post_code : String, country_code: String) - case class KafkaLocationImport( + case class KafkaInboundLocation( latitude : Double, longitude : Double) - case class KafkaUserImport( + case class KafkaInboundUser( email : String, password : String, display_name : String) - case class KafkaValidatedUserImport( + case class KafkaInboundValidatedUser( email : String, display_name : String) - case class KafkaAccountImport( - id : String, - bank : String, - label : String, - number : String, - `type` : String, - balance : KafkaBalanceImport, - IBAN : String, - owners : List[String], - generate_public_view : Boolean, - generate_accountants_view : Boolean, - generate_auditors_view : Boolean) + case class KafkaInboundAccount( + id : String, + bank : String, + label : String, + number : String, + `type` : String, + balance : KafkaInboundBalance, + IBAN : String, + owners : List[String], + generate_public_view : Boolean, + generate_accountants_view : Boolean, + generate_auditors_view : Boolean) - case class KafkaBalanceImport( + case class KafkaInboundBalance( currency : String, amount : String) - case class KafkaTransactionImport( - id : String, - this_account : KafkaAccountIdImport, - counterparty : Option[KafkaTransactionCounterparty], - details : KafkaAccountDetailsImport) + case class KafkaInboundTransaction( + id : String, + this_account : KafkaInboundAccountId, + counterparty : Option[KafkaInboundTransactionCounterparty], + details : KafkaInboundAccountDetails) - case class KafkaTransactionCounterparty( + case class KafkaInboundTransactionCounterparty( name : Option[String], // Also known as Label account_number : Option[String]) - case class KafkaAccountIdImport( + case class KafkaInboundAccountId( id : String, bank : String) - case class KafkaAccountDetailsImport( + case class KafkaInboundAccountDetails( `type` : String, description : String, posted : String, @@ -1036,17 +1057,17 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable value : String) - case class KafkaAtmImport( - id : String, - bank_id: String, - name : String, - address : KafkaAddressImport, - location : KafkaLocationImport, - meta : KafkaMetaImport + case class KafkaInboundAtm( + id : String, + bank_id: String, + name : String, + address : KafkaInboundAddress, + location : KafkaInboundLocation, + meta : KafkaInboundMeta ) - case class KafkaProductImport( + case class KafkaInboundProduct( bank_id : String, code: String, name : String, @@ -1054,39 +1075,39 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable family : String, super_family : String, more_info_url : String, - meta : KafkaMetaImport + meta : KafkaInboundMeta ) - case class KafkaAccountDataImport( - banks : List[KafkaBankImport], - users : List[KafkaUserImport], - accounts : List[KafkaAccountImport] + case class KafkaInboundAccountData( + banks : List[KafkaInboundBank], + users : List[KafkaInboundUser], + accounts : List[KafkaInboundAccount] ) - case class KafkaDataImport( - banks : List[KafkaBankImport], - users : List[KafkaUserImport], - accounts : List[KafkaAccountImport], - transactions : List[KafkaTransactionImport], - branches: List[KafkaBranchImport], - atms: List[KafkaAtmImport], - products: List[KafkaProductImport], - crm_events: List[KafkaCrmEventImport] + case class KafkaInboundData( + banks : List[KafkaInboundBank], + users : List[KafkaInboundUser], + accounts : List[KafkaInboundAccount], + transactions : List[KafkaInboundTransaction], + branches: List[KafkaInboundBranch], + atms: List[KafkaInboundAtm], + products: List[KafkaInboundProduct], + crm_events: List[KafkaInboundCrmEvent] ) - case class KafkaCrmEventImport ( + case class KafkaInboundCrmEvent( id : String, // crmEventId bank_id : String, - customer: KafkaCustomerImport, + customer: KafkaInboundCustomer, category : String, detail : String, channel : String, actual_date: String ) - case class KafkaCustomerImport ( + case class KafkaInboundCustomer( name: String, number : String // customer number, also known as ownerId (owner of accounts) aka API User? ) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index c41dca509..970532951 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -33,7 +33,7 @@ package code.model.dataAccess import code.api.{DirectLogin, OAuthHandshake} import code.bankconnectors.KafkaMappedConnector -import code.bankconnectors.KafkaMappedConnector.KafkaUserImport +import code.bankconnectors.KafkaMappedConnector.KafkaInboundUser import net.liftweb.common._ import net.liftweb.http.js.JsCmds.FocusOnLoad import net.liftweb.http.{S, SHtml, SessionVar, Templates} @@ -287,7 +287,7 @@ import net.liftweb.util.Helpers._ def getExternalUser(username: String, password: String):Box[OBPUser] = { KafkaMappedConnector.getUser(username, password) match { - case Full(KafkaUserImport(extEmail, extPassword, extDisplayName)) => { + case Full(KafkaInboundUser(extEmail, extPassword, extDisplayName)) => { val preLoginState = capturePreLoginState() info("external user authenticated. login redir: " + loginRedirect.get) val redir = loginRedirect.get match { From 93b98695a644c23845a4ca311d71cf2f1b34dd5d Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 14 May 2016 05:44:44 +0200 Subject: [PATCH 448/702] English in comments. --- .../code/bankconnectors/KafkaMappedConnector.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 63b8275d0..b85bbe7c3 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -42,7 +42,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val cachedUserAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) def getUser( username: String, password: String ): Box[KafkaInboundUser] = { - // Generate random uuid to be used as request-respose match id + // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Send request to Kafka, marked with reqId // so we can fetch the corresponding response @@ -87,7 +87,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def updateUserAccountViews( apiUser: APIUser ) = { - // Generate random uuid to be used as request-respose match id + // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId // in order to fetch corresponding response @@ -110,7 +110,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def updatePublicAccountViews( user: APIUser ): List[List[Saveable[ViewType]]] = { - // Generate random uuid to be used as request-respose match id + // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId // in order to fetch corresponding response @@ -192,7 +192,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Gets transaction identified by bankid, accountid and transactionId def getTransaction(bankId: BankId, accountID: AccountId, transactionId: TransactionId): Box[Transaction] = { - // Generate random uuid to be used as request-respose match id + // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId // in order to fetch corresponding response @@ -249,7 +249,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } override def getBankAccountType(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { - // Generate random uuid to be used as request-respose match id + // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId // in order to fetch corresponding response @@ -1085,6 +1085,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable accounts : List[KafkaInboundAccount] ) + // We won't need this. TODO clean up. case class KafkaInboundData( banks : List[KafkaInboundBank], users : List[KafkaInboundUser], From 91703c8a70d46979be3f2c81e654f7e451370168 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Sat, 14 May 2016 12:13:44 +0200 Subject: [PATCH 449/702] Issue #31 --- .../ResourceDocsAPIMethods.scala | 7 +- .../SwaggerJSONFactory.scala | 71 ++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 305f9a232..a74fd4798 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -1,10 +1,12 @@ package code.api.ResourceDocs1_4_0 +import code.api.ResourceDocs1_4_0.SwaggerJSONFactory.SwaggerResourceDoc import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0} +import net.liftweb import net.liftweb.common.{Empty, Box, Full, Loggable} import net.liftweb.http.rest.RestHelper import net.liftweb.http.{S, JsonResponse, Req} -import net.liftweb.json.Extraction +import net.liftweb.json._ import net.liftweb.json.JsonAST.JValue import net.liftweb.json.JsonDSL._ import net.liftweb.util.Props @@ -244,7 +246,8 @@ def filterResourceDocs(allResources: List[ResourceDoc]) : List[ResourceDoc] = { // Format the data as json val json = SwaggerJSONFactory.createSwaggerResourceDoc(rdFiltered, requestedApiVersion) // Return - successJsonResponse(Extraction.decompose(json)) + val jsonAST = SwaggerJSONFactory.loadDefinitions(rdFiltered) + successJsonResponse(Extraction.decompose(json) merge jsonAST) } } } diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index 99ccf7d82..2ee053219 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -1,12 +1,16 @@ package code.api.ResourceDocs1_4_0 -import code.api.util.APIUtil.ResourceDoc import code.api.Constant._ +import code.api.util.APIUtil.ResourceDoc +import net.liftweb +import net.liftweb.json.Extraction._ import net.liftweb.json._ import net.liftweb.util.Props import org.pegdown.PegDownProcessor import scala.collection.immutable.ListMap +import scala.reflect.runtime.currentMirror +import scala.reflect.runtime.universe._ object SwaggerJSONFactory { @@ -53,6 +57,14 @@ object SwaggerJSONFactory { ) def createSwaggerResourceDoc(resourceDocList: List[ResourceDoc], requestedApiVersion: String): SwaggerResourceDoc = { + + def getName(rd: ResourceDoc) = { + rd.apiFunction match { + case "allBanks" => Some(ResponseObjectSchemaJson("#/definitions/BankJSON")) + case _ => None + } + } + implicit val formats = DefaultFormats val pegDownProcessor : PegDownProcessor = new PegDownProcessor @@ -73,7 +85,7 @@ object SwaggerJSONFactory { rd.summary, description = pegDownProcessor.markdownToHtml(rd.description.stripMargin).replaceAll("\n", ""), s"${rd.apiVersion.toString}-${rd.apiFunction.toString}", - Map("200" -> ResponseObjectJson(Some("Success") , None), "400" -> ResponseObjectJson(Some("Error"), Some(ResponseObjectSchemaJson("#/definitions/Error")))))) + Map("200" -> ResponseObjectJson(Some("Success") , getName(rd)), "400" -> ResponseObjectJson(Some("Error"), Some(ResponseObjectSchemaJson("#/definitions/Error")))))) ).toMap (mrd._1, methods.toSeq.sortBy(m => m._1).toMap) }(collection.breakOut) @@ -89,4 +101,59 @@ object SwaggerJSONFactory { SwaggerResourceDoc("2.0", info, host, basePath, schemas, paths, defs) } + + def translateEntity(entity: Any) = { + + val r = currentMirror.reflect(entity) + val ddd = r.symbol.typeSignature.members.toStream + .collect { case s: TermSymbol if !s.isMethod => r.reflectField(s)} + .map(r => r.symbol.name.toString.trim -> r.get) + .toMap + + val properties = for ((key, value) <- ddd) yield { + value match { + case i: String => "\"" + key + "\":" + """{"type":"string"}""" + case Some(i: String) => "\"" + key + "\":" + """{"type":"string"}""" + case i: Int => "\"" + key + "\":" + """{"type":"integer", "format":"int32"}""" + case Some(i: Int) => "\"" + key + "\":" + """{"type":"integer", "format":"int32"}""" + case i: Long => "\"" + key + "\":" + """{"type":"integer", "format":"int64"}""" + case Some(i: Long) => "\"" + key + "\":" + """{"type":"integer", "format":"int64"}""" + case i: Float => "\"" + key + "\":" + """{"type":"number", "format":"float"}""" + case Some(i: Long) => "\"" + key + "\":" + """{"type":"number", "format":"float"}""" + case i: Double => "\"" + key + "\":" + """{"type":"number", "format":"double"}""" + case Some(i: Double) => "\"" + key + "\":" + """{"type":"number", "format":"double"}""" + case _ => "unknown" + } + } + val fields: String = properties filter (_.contains("unknown") == false) mkString (",") + + val required = for {f <- entity.getClass.getDeclaredFields + if f.getType.toString.contains("Option") == false + } yield { + f.getName + } + val requiredFields = required.toList mkString("[\"", "\",\"", "\"]") + + val definition = "\"" + entity.getClass.getSimpleName + "\":" + """{"required": """ + requiredFields + "," + """"properties": {""" + fields + """}}""" + + definition + + } + + def loadDefinitions (resourceDocList: List[ResourceDoc]): liftweb.json.JValue = { + + import code.api.v1_2_1._ + + implicit val formats = DefaultFormats + val jsonAST1: JValue = decompose(BankJSON("1", "Name", "Name1", "None", "www.go.com")) + val jsonCaseClass1 = jsonAST1.extract[BankJSON] + + val jsonAST2: JValue = decompose(UserJSON("1", "Name", "Name1")) + val jsonCaseClass2 = jsonAST2.extract[UserJSON] + + val definitions = "{\"definitions\":{" + translateEntity(jsonCaseClass1) + "," + translateEntity(jsonCaseClass2) + "}}" + + parse(definitions) + } + } \ No newline at end of file From 7f392adf78a0dbba8b67b5e24b9b411670ddbbee Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Tue, 17 May 2016 10:56:07 +0200 Subject: [PATCH 450/702] Issue #31, added more primitive types and lists of they --- .../SwaggerJSONFactory.scala | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index 2ee053219..67ce40db6 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -1,5 +1,7 @@ package code.api.ResourceDocs1_4_0 +import java.util.Date + import code.api.Constant._ import code.api.util.APIUtil.ResourceDoc import net.liftweb @@ -102,7 +104,7 @@ object SwaggerJSONFactory { } - def translateEntity(entity: Any) = { + def translateEntity(entity: Any): String = { val r = currentMirror.reflect(entity) val ddd = r.symbol.typeSignature.members.toStream @@ -112,16 +114,34 @@ object SwaggerJSONFactory { val properties = for ((key, value) <- ddd) yield { value match { + case i: Boolean => "\"" + key + "\":" + """{"type":"boolean"}""" + case Some(i: Boolean) => "\"" + key + "\":" + """{"type":"boolean"}""" + case List(i: Boolean, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "boolean"}}""" + case Some(List(i: Boolean, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "boolean"}}""" case i: String => "\"" + key + "\":" + """{"type":"string"}""" case Some(i: String) => "\"" + key + "\":" + """{"type":"string"}""" + case List(i: String, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "string"}}""" + case Some(List(i: String, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "string"}}""" case i: Int => "\"" + key + "\":" + """{"type":"integer", "format":"int32"}""" case Some(i: Int) => "\"" + key + "\":" + """{"type":"integer", "format":"int32"}""" + case List(i: Long, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"integer", "format":"int32"}}""" + case Some(List(i: Long, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"integer", "format":"int32"}}""" case i: Long => "\"" + key + "\":" + """{"type":"integer", "format":"int64"}""" case Some(i: Long) => "\"" + key + "\":" + """{"type":"integer", "format":"int64"}""" + case List(i: Long, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"integer", "format":"int64"}}""" + case Some(List(i: Long, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"integer", "format":"int64"}}""" case i: Float => "\"" + key + "\":" + """{"type":"number", "format":"float"}""" - case Some(i: Long) => "\"" + key + "\":" + """{"type":"number", "format":"float"}""" + case Some(i: Float) => "\"" + key + "\":" + """{"type":"number", "format":"float"}""" + case List(i: Float, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "float"}}""" + case Some(List(i: Float, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "float"}}""" case i: Double => "\"" + key + "\":" + """{"type":"number", "format":"double"}""" case Some(i: Double) => "\"" + key + "\":" + """{"type":"number", "format":"double"}""" + case List(i: Double, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "double"}}""" + case Some(List(i: Double, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type": "double"}}""" + case i: Date => "\"" + key + "\":" + """{"type":"string", "format":"date"}""" + case Some(i: Date) => "\"" + key + "\":" + """{"type":"string", "format":"date"}""" + case List(i: Date, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"string", "format":"date"}}""" + case Some(List(i: Date, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"string", "format":"date"}}""" case _ => "unknown" } } @@ -134,7 +154,9 @@ object SwaggerJSONFactory { } val requiredFields = required.toList mkString("[\"", "\",\"", "\"]") - val definition = "\"" + entity.getClass.getSimpleName + "\":" + """{"required": """ + requiredFields + "," + """"properties": {""" + fields + """}}""" + val requiredFieldsPart = if (required.length > 0) """"required": """ + requiredFields + "," else "" + + val definition = "\"" + entity.getClass.getSimpleName + "\":{" + requiredFieldsPart + """"properties": {""" + fields + """}}""" definition From f1b38a609c380bbd471fb758b90688d43e8411ae Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 18 May 2016 10:14:13 +0200 Subject: [PATCH 451/702] Issue #31, added support for some particular object types and lists of they --- .../ResourceDocs1_4_0/SwaggerJSONFactory.scala | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index 67ce40db6..968b94d48 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -4,6 +4,7 @@ import java.util.Date import code.api.Constant._ import code.api.util.APIUtil.ResourceDoc +import code.api.v1_2.{BankJSON, BanksJSON, UserJSON} import net.liftweb import net.liftweb.json.Extraction._ import net.liftweb.json._ @@ -62,7 +63,7 @@ object SwaggerJSONFactory { def getName(rd: ResourceDoc) = { rd.apiFunction match { - case "allBanks" => Some(ResponseObjectSchemaJson("#/definitions/BankJSON")) + case "allBanks" => Some(ResponseObjectSchemaJson("#/definitions/BanksJSON")) case _ => None } } @@ -142,6 +143,12 @@ object SwaggerJSONFactory { case Some(i: Date) => "\"" + key + "\":" + """{"type":"string", "format":"date"}""" case List(i: Date, _*) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"string", "format":"date"}}""" case Some(List(i: Date, _*)) => "\"" + key + "\":" + """{"type":"array", "items":{"type":"string", "format":"date"}}""" + case obj@BankJSON(_,_,_,_,_) => "\"" + key + "\":{" + """"$ref": "#/definitions/BankJSON"""" +"}" + case obj@List(BankJSON(_,_,_,_,_)) => "\"" + key + "\":" + """{"type":"array", "items":{"$ref": "#/definitions/BankJSON"""" +"}}" + case obj@BanksJSON(_) => "\"" + key + "\":{" + """"$ref": "#/definitions/BanksJSON"""" +"}" + case obj@List(BanksJSON(_)) => "\"" + key + "\":" + """{"type":"array", "items":{"$ref": "#/definitions/BanksJSON"""" +"}}" + case obj@UserJSON(_,_,_) => "\"" + key + "\":{" + """"$ref": "#/definitions/UserJSON"""" +"}" + case obj@List(UserJSON(_,_,_)) => "\"" + key + "\":" + """{"type":"array", "items":{"$ref": "#/definitions/UserJSON"""" +"}}" case _ => "unknown" } } @@ -164,8 +171,6 @@ object SwaggerJSONFactory { def loadDefinitions (resourceDocList: List[ResourceDoc]): liftweb.json.JValue = { - import code.api.v1_2_1._ - implicit val formats = DefaultFormats val jsonAST1: JValue = decompose(BankJSON("1", "Name", "Name1", "None", "www.go.com")) val jsonCaseClass1 = jsonAST1.extract[BankJSON] @@ -173,7 +178,9 @@ object SwaggerJSONFactory { val jsonAST2: JValue = decompose(UserJSON("1", "Name", "Name1")) val jsonCaseClass2 = jsonAST2.extract[UserJSON] - val definitions = "{\"definitions\":{" + translateEntity(jsonCaseClass1) + "," + translateEntity(jsonCaseClass2) + "}}" + val caseClass3 = BanksJSON(List(BankJSON("1", "Name", "Name1", "None", "www.go.com"))) + + val definitions = "{\"definitions\":{" + translateEntity(jsonCaseClass1) + "," + translateEntity(jsonCaseClass2) + "," + translateEntity(caseClass3) + "}}" parse(definitions) } From 4f7cedc27dac579275391539e7fa41948aaa57df Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 18 May 2016 12:00:26 +0200 Subject: [PATCH 452/702] Code and prop templates cleanup --- .../resources/props/sample.props.template | 6 ++-- .../props/test.default.props.template | 5 +++- .../code/bankconnectors/KafkaHelper.scala | 30 +++++++------------ .../bankconnectors/KafkaMappedConnector.scala | 13 ++++---- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 7c8e39d84..e8d89e7e7 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -13,17 +13,19 @@ connector=mapped #connector=... #if using kafka connector, set zookeeper host -#defaults to "localhost:2181" if not set #kafka.zookeeper_host=123.45.67.89:2181 +#cache time to live in seconds, caching disabled if not set +#kafka.cache.ttl.seconds=3 + #if using kafka connector, the following is mandatory -#kafka.group_id=1 #kafka.request_topic=Request #kafka.response_topic=Response #you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url #db.driver=org.postgresql.Driver #db.driver=org.h2.Driver +#db.url=jdbc:h2:./lift_proto.db;DB_CLOSE_ON_EXIT=FALSE #be sure to create your database and update the line below! db.url=jdbc:postgresql://localhost:5432/dbname?user=dbusername&password=thepassword diff --git a/src/main/resources/props/test.default.props.template b/src/main/resources/props/test.default.props.template index 1e4eead3f..61524b5b2 100644 --- a/src/main/resources/props/test.default.props.template +++ b/src/main/resources/props/test.default.props.template @@ -15,8 +15,10 @@ connector=mapped #defaults to "localhost:2181" if not set #kafka.zookeeper_host=123.45.67.89:2181 +#cache time to live in seconds, caching disabled if not set +#kafka.cache.ttl.seconds=3 + #if using kafka connector, the following is mandatory -#kafka.group_id=1 #kafka.request_topic=Request #kafka.response_topic=Response @@ -38,6 +40,7 @@ End of minimum settings #if connector is mapped, set a database backend. If not set, this will be set to an in-memory h2 database by default #you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url #db.driver=org.h2.Driver +#db.url=jdbc:h2:./lift_proto.db;DB_CLOSE_ON_EXIT=FALSE #set this to false if you don't want the api payments call to work payments_enabled=false diff --git a/src/main/scala/code/bankconnectors/KafkaHelper.scala b/src/main/scala/code/bankconnectors/KafkaHelper.scala index 9e0cd0e35..5b2f23d31 100644 --- a/src/main/scala/code/bankconnectors/KafkaHelper.scala +++ b/src/main/scala/code/bankconnectors/KafkaHelper.scala @@ -57,7 +57,6 @@ object ZooKeeperUtils { } class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").openOrThrowException("no kafka.zookeeper_host set"), - val groupId: String = Props.get("kafka.group_id").openOrThrowException("no kafka.group_id set"), val topic: String = Props.get("kafka.response_topic").openOrThrowException("no kafka.response_topic set"), val delay: Long = 0) { @@ -65,17 +64,6 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").op zkProps.put("log4j.logger.org.apache.zookeeper", "ERROR") org.apache.log4j.PropertyConfigurator.configure(zkProps) - val config = createConsumerConfig(zookeeper, groupId) - val consumer = Consumer.create(config) - - // create single stream for topic - var consumerMap = consumer.createMessageStreams(Map(topic -> 1)) - - def shutdown() = { - if (consumer != null) - consumer.shutdown() - } - def createConsumerConfig(zookeeper: String, groupId: String): ConsumerConfig = { val props = new Properties() props.put("zookeeper.connect", zookeeper) @@ -86,16 +74,18 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").op props.put("auto.commit.interval.ms", "1000") props.put("zookeeper.session.timeout.ms", "6000") props.put("zookeeper.connection.timeout.ms", "6000") - props.put("consumer.timeout.ms", "20000") + props.put("consumer.timeout.ms", "40000") val config = new ConsumerConfig(props) config } - def getResponse(reqId: String): json.JValue = { //List[Map[String, String]] = { + def getResponse(reqId: String): json.JValue = { + // create consumer with unique groupId in order to prevent race condition with kafka + val config = createConsumerConfig(zookeeper, UUID.randomUUID.toString) + val consumer = Consumer.create(config) // recreate stream for topic if not existing - if ( consumerMap == null ) { - consumerMap = consumer.createMessageStreams(Map(topic -> 1)) - } + val consumerMap = consumer.createMessageStreams(Map(topic -> 1)) + val streams = consumerMap.get(topic).get // process streams for (stream <- streams) { @@ -110,18 +100,20 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").op if (key == reqId) { // Parse JSON message val j = json.parse(msg) + // disconnect from Kafka + consumer.shutdown() // return as JSON return j } } } catch { - case e:kafka.consumer.ConsumerTimeoutException => println("Exception: " + e.toString()) + case e:kafka.consumer.ConsumerTimeoutException => println("Exception: " + e.toString) return json.parse("""{"error":"timeout"}""") //TODO: replace with standard message } } // disconnect from kafka - shutdown() + consumer.shutdown() return json.parse("""{"info":"disconnected"}""") //TODO: replace with standard message } } diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index b85bbe7c3..76f24fec8 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -32,7 +32,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable var consumer = new KafkaConsumer() type AccountType = KafkaBankAccount - val cacheTTL = Props.get("cache.ttl.base", "30").toInt + // Local TTL Cache + val cacheTTL = Props.get("kafka.cache.ttl.seconds", "3").toInt val cachedUser = TTLCache[KafkaInboundValidatedUser](cacheTTL) val cachedBank = TTLCache[KafkaInboundBank](cacheTTL) val cachedAccount = TTLCache[KafkaInboundAccount](cacheTTL) @@ -866,11 +867,11 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Helper for creating a transaction def createNewTransaction(r: KafkaInboundTransaction) = { var datePosted: Date = null - if (r.details.posted != null && r.details.posted.matches("^[0-9]{8}$")) + if (r.details.posted != null) // && r.details.posted.matches("^[0-9]{8}$")) datePosted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.posted) var dateCompleted: Date = null - if (r.details.completed != null && r.details.completed.matches("^[0-9]{8}$")) + if (r.details.completed != null) // && r.details.completed.matches("^[0-9]{8}$")) dateCompleted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.completed) val o = getBankAccountType(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).get @@ -884,7 +885,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable r.id, // uuid:String TransactionId(r.id), // id:TransactionId getBankAccountType( BankId(r.this_account.bank), - AccountId(r.this_account.id)).get, // thisAccount:BankAccount + AccountId(r.this_account.id)).orNull, // thisAccount:BankAccount otherAccount, // otherAccount:OtherBankAccount r.details.`type`, // transactionType:String BigDecimal(r.details.value), // val amount:BigDecimal @@ -1038,7 +1039,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable id : String, this_account : KafkaInboundAccountId, counterparty : Option[KafkaInboundTransactionCounterparty], - details : KafkaInboundAccountDetails) + details : KafkaInboundTransactionDetails) case class KafkaInboundTransactionCounterparty( name : Option[String], // Also known as Label @@ -1048,7 +1049,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable id : String, bank : String) - case class KafkaInboundAccountDetails( + case class KafkaInboundTransactionDetails( `type` : String, description : String, posted : String, From efb6dad83983d13a8d960936a017a5b9e6ca5c6c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 18 May 2016 12:00:49 +0200 Subject: [PATCH 453/702] Refactor: Internal rename: allBanks to getBanks --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 4 ++-- src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala | 2 +- src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala | 2 +- src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala | 2 +- src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index c3d5e18f6..a2fc44a51 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -113,7 +113,7 @@ trait APIMethods121 { resourceDocs += ResourceDoc( - allBanks, + getBanks, apiVersion, "allBanks", "GET", @@ -133,7 +133,7 @@ trait APIMethods121 { true, apiTagBanks :: Nil) - lazy val allBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + lazy val getBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get banks case "banks" :: Nil JsonGet json => { user => diff --git a/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala index ad78adfc4..7567a792c 100644 --- a/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala +++ b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala @@ -45,7 +45,7 @@ object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable { val routes = List( Implementations1_2_1.root(VERSION), - Implementations1_2_1.allBanks, + Implementations1_2_1.getBanks, Implementations1_2_1.bankById, Implementations1_2_1.allAccountsAllBanks, Implementations1_2_1.privateAccountsAllBanks, diff --git a/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala b/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala index 380dbaca3..9f155670b 100644 --- a/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala +++ b/src/main/scala/code/api/v1_3_0/OBPAPI1_3_0.scala @@ -16,7 +16,7 @@ object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 w //TODO: check all these calls to see if they should really have the same behaviour as 1.2.1 val routes = List( Implementations1_2_1.root(VERSION), - Implementations1_2_1.allBanks, + Implementations1_2_1.getBanks, Implementations1_2_1.bankById, Implementations1_2_1.allAccountsAllBanks, Implementations1_2_1.privateAccountsAllBanks, diff --git a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala index 916bc2ea3..55a4f2459 100644 --- a/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/OBPAPI1_4_0.scala @@ -9,7 +9,7 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable { val routes = List( Implementations1_2_1.root(VERSION), - Implementations1_2_1.allBanks, + Implementations1_2_1.getBanks, Implementations1_2_1.bankById, Implementations1_2_1.allAccountsAllBanks, Implementations1_2_1.privateAccountsAllBanks, diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index fc84d529d..4e6084246 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -46,7 +46,7 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w val routes = List( Implementations1_2_1.root(VERSION), - Implementations1_2_1.allBanks, + Implementations1_2_1.getBanks, Implementations1_2_1.bankById, // Now in 2_0_0 // Implementations1_2_1.allAccountsAllBanks, From 1816a86c51ce2ec5aebd32b89a7313c1a5593414 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 18 May 2016 16:58:02 +0200 Subject: [PATCH 454/702] Swithed to maven3 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 15a9e0788..bd7c8f6ea 100644 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,9 @@ https://oss.sonatype.org/content/groups/scala-tools/ - java.net.maven2 - java.net Maven2 Repository - http://download.java.net/maven/2/ + java.net.maven3 + java.net Maven3 Repository + http://download.java.net/maven/3/ scala-tools.snapshots From 1adb333fd8e4a2de3d99372409dc516aa6c678d2 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 18 May 2016 17:04:25 +0200 Subject: [PATCH 455/702] Disabled git-commit-id-plugin until resolved --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bd7c8f6ea..03c976312 100644 --- a/pom.xml +++ b/pom.xml @@ -352,7 +352,7 @@ - + From 2ec349bb3e17869788d631a474d98765c28ab2e9 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 19 May 2016 11:01:54 +0200 Subject: [PATCH 456/702] Issue #31, inserted comments --- .../ResourceDocsAPIMethods.scala | 3 +- .../SwaggerJSONFactory.scala | 58 +++++++++++++------ .../scala/code/api/v1_2_1/APIMethods121.scala | 6 +- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index a74fd4798..999c4157e 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -245,8 +245,9 @@ def filterResourceDocs(allResources: List[ResourceDoc]) : List[ResourceDoc] = { val rdFiltered = filterResourceDocs(rd) // Format the data as json val json = SwaggerJSONFactory.createSwaggerResourceDoc(rdFiltered, requestedApiVersion) - // Return + //Get definitions of objects of success responses val jsonAST = SwaggerJSONFactory.loadDefinitions(rdFiltered) + // Merge both results and return successJsonResponse(Extraction.decompose(json) merge jsonAST) } } diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index 968b94d48..50c1bb73a 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -64,6 +64,7 @@ object SwaggerJSONFactory { def getName(rd: ResourceDoc) = { rd.apiFunction match { case "allBanks" => Some(ResponseObjectSchemaJson("#/definitions/BanksJSON")) + case "bankById" => Some(ResponseObjectSchemaJson("#/definitions/BankJSON")) case _ => None } } @@ -107,13 +108,15 @@ object SwaggerJSONFactory { def translateEntity(entity: Any): String = { + //Get fields of runtime entities and put they into structure Map(nameOfField -> fieldAsObject) val r = currentMirror.reflect(entity) - val ddd = r.symbol.typeSignature.members.toStream + val mapOfFields = r.symbol.typeSignature.members.toStream .collect { case s: TermSymbol if !s.isMethod => r.reflectField(s)} .map(r => r.symbol.name.toString.trim -> r.get) .toMap - val properties = for ((key, value) <- ddd) yield { + //Iterate over Map and use pattern matching to extract type of field of runtime entity and make an appropriate swagger string for it + val properties = for ((key, value) <- mapOfFields) yield { value match { case i: Boolean => "\"" + key + "\":" + """{"type":"boolean"}""" case Some(i: Boolean) => "\"" + key + "\":" + """{"type":"boolean"}""" @@ -152,36 +155,53 @@ object SwaggerJSONFactory { case _ => "unknown" } } + //Exclude all unrecognised fields and make part of fields definition val fields: String = properties filter (_.contains("unknown") == false) mkString (",") - - val required = for {f <- entity.getClass.getDeclaredFields - if f.getType.toString.contains("Option") == false - } yield { - f.getName - } + //Collect all mandatory fields and make an appropriate string + val required = + for { + f <- entity.getClass.getDeclaredFields + if f.getType.toString.contains("Option") == false + } yield { + f.getName + } val requiredFields = required.toList mkString("[\"", "\",\"", "\"]") - + //Make part of mandatory fields val requiredFieldsPart = if (required.length > 0) """"required": """ + requiredFields + "," else "" - + //Make whole swagger definition of an entity val definition = "\"" + entity.getClass.getSimpleName + "\":{" + requiredFieldsPart + """"properties": {""" + fields + """}}""" definition } - def loadDefinitions (resourceDocList: List[ResourceDoc]): liftweb.json.JValue = { + def loadDefinitions(resourceDocList: List[ResourceDoc]): liftweb.json.JValue = { implicit val formats = DefaultFormats - val jsonAST1: JValue = decompose(BankJSON("1", "Name", "Name1", "None", "www.go.com")) - val jsonCaseClass1 = jsonAST1.extract[BankJSON] - val jsonAST2: JValue = decompose(UserJSON("1", "Name", "Name1")) - val jsonCaseClass2 = jsonAST2.extract[UserJSON] - - val caseClass3 = BanksJSON(List(BankJSON("1", "Name", "Name1", "None", "www.go.com"))) - - val definitions = "{\"definitions\":{" + translateEntity(jsonCaseClass1) + "," + translateEntity(jsonCaseClass2) + "," + translateEntity(caseClass3) + "}}" + //Translate a jsonAST to an appropriate case class entity + val successResponseBodies: List[Any] = + for (rd <- resourceDocList) + yield { + rd match { + case u if u.apiFunction.contains("allBanks") => rd.successResponseBody.extract[BanksJSON] + case u if u.apiFunction.contains("bankById") => rd.successResponseBody.extract[BankJSON] + case _ => "Not defined" + } + } + val successResponseBodiesForProcessing = successResponseBodies filter (_.toString().contains("Not defined") == false) + //Translate every entity in a list to appropriate swagger format + val listOfParticularDefinition = + for (e <- successResponseBodiesForProcessing) + yield { + translateEntity(e) + } + //Add a comma between elements of a list and make a string + val particularDefinitionsPart = listOfParticularDefinition mkString (",") + //Make a final string + val definitions = "{\"definitions\":{" + particularDefinitionsPart + "}}" + //Make a jsonAST from a string parse(definitions) } diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index a2fc44a51..f52291a37 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -1,10 +1,12 @@ package code.api.v1_2_1 import code.api.util.APIUtil +import code.api.v1_2.{BanksJSON, BankJSON} import net.liftweb.http.{JsonResponse, Req} import net.liftweb.json.Extraction import net.liftweb.common._ import code.model._ +import net.liftweb.json.Extraction._ import net.liftweb.json.JsonAST.JValue import APIUtil._ import net.liftweb.util.Helpers._ @@ -126,7 +128,7 @@ trait APIMethods121 { |* Logo URL |* Website""", emptyObjectJson, - emptyObjectJson, + decompose(BanksJSON(List(BankJSON("1", "EFG", "Eurobank", "None", "www.eurobank.rs")))), emptyObjectJson :: Nil, true, false, @@ -163,7 +165,7 @@ trait APIMethods121 { |* Logo URL |* Website""", emptyObjectJson, - emptyObjectJson, + decompose(BankJSON("1", "EFG", "Eurobank", "None", "www.eurobank.rs")), emptyObjectJson :: Nil, true, false, From 96791464b961df9fc9b91ef8d0342fd476b81095 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 19 May 2016 14:30:38 +0200 Subject: [PATCH 457/702] Issue #31, removed unused imports --- .../scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala | 1 - src/main/scala/code/api/v1_2_1/APIMethods121.scala | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala index 50c1bb73a..deaedc59f 100644 --- a/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala +++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala @@ -6,7 +6,6 @@ import code.api.Constant._ import code.api.util.APIUtil.ResourceDoc import code.api.v1_2.{BankJSON, BanksJSON, UserJSON} import net.liftweb -import net.liftweb.json.Extraction._ import net.liftweb.json._ import net.liftweb.util.Props import org.pegdown.PegDownProcessor diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index f52291a37..86f6a700c 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -1,7 +1,6 @@ package code.api.v1_2_1 import code.api.util.APIUtil -import code.api.v1_2.{BanksJSON, BankJSON} import net.liftweb.http.{JsonResponse, Req} import net.liftweb.json.Extraction import net.liftweb.common._ From a481eba29dcbe730557df503c78fe0740ab69006 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 20 May 2016 18:14:34 +0200 Subject: [PATCH 458/702] Enabled git-commit-id-plugin in pom.xml --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 03c976312..bd7c8f6ea 100644 --- a/pom.xml +++ b/pom.xml @@ -352,7 +352,7 @@ - + From aa5ab04236c44a5b91427b252103fd7c1d2b549c Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 20 May 2016 18:16:03 +0200 Subject: [PATCH 459/702] Added createUser (endpoint /users) for creating user from api --- .../scala/code/api/v2_0_0/APIMethods200.scala | 67 ++++++++++++++++--- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 19 +++--- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index d512fa7f6..08024574e 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -8,6 +8,8 @@ import code.api.util.ErrorMessages import code.api.v1_2_1.OBPAPI1_2_1._ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121, JSONFactory => JSONFactory121} import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJSON} +import code.model.dataAccess.OBPUser +import net.liftweb.mapper.By //import code.api.v1_4_0.JSONFactory1_4_0._ import code.api.v2_0_0.JSONFactory200._ @@ -92,16 +94,16 @@ trait APIMethods200 { // helper methods end here - val Implementations2_0_0 = new Object(){ + val Implementations2_0_0 = new Object() { val resourceDocs = ArrayBuffer[ResourceDoc]() val apiRelations = ArrayBuffer[ApiRelation]() - val emptyObjectJson : JValue = Nil - val apiVersion : String = "2_0_0" + val emptyObjectJson: JValue = Nil + val apiVersion: String = "2_0_0" - val exampleDateString : String ="22/08/2013" - val simpleDateFormat : SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") + val exampleDateString: String = "22/08/2013" + val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") val exampleDate = simpleDateFormat.parse(exampleDateString) val codeContext = CodeContext(resourceDocs, apiRelations) @@ -131,7 +133,7 @@ trait APIMethods200 { false, false, List(apiTagAccounts, apiTagPrivateData, apiTagPublicData)) - + lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for all banks (private + public) @@ -1256,9 +1258,58 @@ trait APIMethods200 { } + resourceDocs += ResourceDoc( + createUser, + apiVersion, + "createUser", + "POST", + "/users", + "Create User.", + s"""Creates OBP user. + | No authorisation (currently) required. + | + | Mimics current webform to Register. + | + | Requires username(email) and password. + | + | Returns 409 error if username not unique. + | + | Optionally (Default False) require validation of email address. + | + |""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagPayment)) - - + lazy val createUser: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "users" :: Nil JsonPost json -> _ => { + user => + for { + postedData <- tryo {json.extract[CreateUserJSON]} ?~! ErrorMessages.InvalidJsonFormat + } yield { + if (OBPUser.find(By(OBPUser.email, postedData.email)).isEmpty) { + val userCreated:Boolean = OBPUser.create + .firstName(postedData.firstName) + .lastName(postedData.lastName) + .email(postedData.email) + .password(postedData.password) + .validated(false) + .save + if (userCreated) + successJsonResponse(JsRaw("{}"), 201) + else + Full(errorJsonResponse("Error occurred during user creation.")) + } + else { + Full(errorJsonResponse("User with the same email already exists.")) + } + } + } + } } } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index b6adc9e31..c46e45de0 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -33,24 +33,22 @@ package code.api.v2_0_0 import java.net.URL import java.util.Date + import code.TransactionTypes.TransactionType.TransactionType import code.transactionrequests.TransactionRequests._ // import code.api.util.APIUtil.ApiLink +import code.api.v1_2_1.{AmountOfMoneyJSON, JSONFactory => JSONFactory121, MinimalBankJSON => MinimalBankJSON121, OtherAccountJSON => OtherAccountJSON121, ThisAccountJSON => ThisAccountJSON121, TransactionDetailsJSON => TransactionDetailsJSON121, UserJSON => UserJSON121, ViewJSON => ViewJSON121} +import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeJSON, TransactionRequestAccountJSON} +import code.kycchecks.KycCheck import code.kycdocuments.KycDocument import code.kycmedias.KycMedia import code.kycstatuses.KycStatus -import code.kycchecks.KycCheck -import code.socialmedia.SocialMedia -import net.liftweb.common.{Box, Full} import code.model._ +import code.socialmedia.SocialMedia import net.liftweb.json.JsonAST.JValue -import code.api.v1_2_1.{AmountOfMoneyJSON, UserJSON => UserJSON121, ViewJSON => ViewJSON121, ThisAccountJSON => ThisAccountJSON121, OtherAccountJSON => OtherAccountJSON121, TransactionDetailsJSON => TransactionDetailsJSON121, JSONFactory => JSONFactory121, MinimalBankJSON => MinimalBankJSON121} - -import code.api.v1_4_0.JSONFactory1_4_0.{TransactionRequestAccountJSON, ChallengeJSON} - // New in 2.0.0 @@ -70,7 +68,12 @@ class ResultAndLinksJSON( val links: LinksJSON ) - +class CreateUserJSON( + val email: String, + val password: String, + val lastName: String, + val firstName: String + ) class BasicViewJSON( val id: String, From 9329ec066cd2c84b10bde6361e9a35e6e51feb2f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 21 May 2016 10:38:52 +0200 Subject: [PATCH 460/702] comment on transaction --- src/main/scala/code/model/MappedTransaction.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/code/model/MappedTransaction.scala b/src/main/scala/code/model/MappedTransaction.scala index 254b1d6a8..2acb70a18 100644 --- a/src/main/scala/code/model/MappedTransaction.scala +++ b/src/main/scala/code/model/MappedTransaction.scala @@ -20,6 +20,7 @@ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK wit override def defaultValue = UUID.randomUUID().toString } //TODO: review the need for this + // (why do we need transactionUUID and transactionId - which is a UUID?) object transactionUUID extends MappedUUID(this) object transactionType extends MappedString(this, 100) From f2aaa11972cec6ec213efe823a21d649cbac91f2 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 21 May 2016 14:49:44 +0200 Subject: [PATCH 461/702] Adding customerId (UUID) to Customer, an additional guard in addCustomer --- README.md | 6 ++++-- src/main/scala/code/api/util/APIUtil.scala | 3 +++ .../scala/code/api/v1_4_0/APIMethods140.scala | 7 ++++--- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 21 ++++++++++++++++++- .../code/customer/CustomerProvider.scala | 8 +++++-- .../customer/MappedCustomerProvider.scala | 20 +++++++++++++++++- .../MappedKycDocumentsProvider.scala | 1 + src/main/scala/code/model/BankingData.scala | 10 +++++++++ .../scala/code/api/v1_4_0/CustomerTest.scala | 11 +++++++--- .../v1_4_0/MappedCustomerMessagesTest.scala | 3 +++ 10 files changed, 78 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0598591b7..6c6e2b28f 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,15 @@ To compile and run jetty, install Maven 3 and execute: * Run a single test. For instance right click on test/scala/code/branches/MappedBranchProviderTest and select Run Mapp... -* Run multiple tests: Right click on code and select Run. If need be: +* Run multiple tests: Right click on test/scala/code and select Run. If need be: Goto Run / Debug configurations Test Kind: Select All in Package Package: Select code - Add the absolute /path-to-your-OBP-API in the "working directory" field + Add the absolute /path-to-your-OBP-API in the "working directory" field + You might need to assign more memory via VM Options: e.g. -Xmx1512M -XX:MaxPermSize=512M Make sure your test.default.props has the minimum settings (see test.default.props.template) + Right click test/scala/code and select the Scala Tests in code to run them all. diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 655c75305..eff4bc697 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -83,6 +83,9 @@ object ErrorMessages { val ViewNotFound = "OBP-30005: View not found for Account. Please specify a valid value for VIEW_ID" + val CustomerNumberAlreadyExists = "OBP-30006: Customer Number already exists. Please specify a different value for BANK_ID or CUSTOMER_NUMBER." + val CustomerAlreadyExistsForUser = "OBP-30007: The User is already linked to a Customer at BANK_ID" + // Transaction related messages: val InvalidTransactionRequestType = "OBP-40001: Invalid value for TRANSACTION_REQUEST_TYPE" diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 94508682e..b32ee0a55 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -570,7 +570,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |Dates need to be in the format 2013-01-21T23:08:00Z |OAuth authentication is required. |""", - Extraction.decompose(CustomerJson("687687678", "Joe David Bloggs", + Extraction.decompose(PostCustomerJson("687687678", "Joe David Bloggs", "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), exampleDate, "Single", 1, List(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)), emptyObjectJson, @@ -587,8 +587,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ for { u <- user ?~! "User must be logged in to post Customer" bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, u).isEmpty) ?~ "Customer already exists for this user." - postedData <- tryo{json.extract[CustomerJson]} ?~! "Incorrect json format" + customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, u).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser + postedData <- tryo{json.extract[PostCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat + checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists customer <- Customer.customerProvider.vend.addCustomer(bankId, u, postedData.customer_number, diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 94ed83303..2aa3f62a3 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -22,7 +22,25 @@ import code.api.v1_2_1.{AmountOfMoneyJSON} object JSONFactory1_4_0 { - case class CustomerJson(customer_number : String, + + case class PostCustomerJson( + customer_number : String, + legal_name : String, + mobile_phone_number : String, + email : String, + face_image : CustomerFaceImageJson, + date_of_birth: Date, + relationship_status: String, + dependants: Int, + dob_of_dependants: List[Date], + highest_education_attained: String, + employment_status: String, + kyc_status: Boolean, + last_ok_date: Date) + + + case class CustomerJson(customer_id: String, + customer_number : String, legal_name : String, mobile_phone_number : String, email : String, @@ -83,6 +101,7 @@ object JSONFactory1_4_0 { def createCustomerJson(cInfo : Customer) : CustomerJson = { CustomerJson( + customer_id = cInfo.customerId, customer_number = cInfo.number, legal_name = cInfo.legalName, mobile_phone_number = cInfo.mobileNumber, diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala index 785217192..9bb06c3ac 100644 --- a/src/main/scala/code/customer/CustomerProvider.scala +++ b/src/main/scala/code/customer/CustomerProvider.scala @@ -17,7 +17,10 @@ object Customer extends SimpleInjector { trait CustomerProvider { def getCustomer(bankId : BankId, user : User) : Box[Customer] - def getUser(bankId : BankId, customerId : String) : Box[User] + def getUser(bankId : BankId, customerNumber : String) : Box[User] + + def checkCustomerNumberAvailable(bankId : BankId, customerNumber : String) : Boolean + def addCustomer(bankId: BankId, user: User, number: String, legalName: String, mobileNumber: String, email: String, faceImage: CustomerFaceImage, dateOfBirth: Date, @@ -32,7 +35,8 @@ trait CustomerProvider { } trait Customer { - def number : String + def customerId : String // The UUID for the customer. To be used in URLs + def number : String // The Customer number i.e. the bank identifier for the customer. def legalName : String def mobileNumber : String def email : String diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 4ee76ebb1..9c0c1cdb4 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -4,12 +4,27 @@ import java.util.Date import code.model.{BankId, User} import code.model.dataAccess.APIUser -import code.util.DefaultStringField +import code.util.{MappedUUID, DefaultStringField} import net.liftweb.common.Box import net.liftweb.mapper._ object MappedCustomerProvider extends CustomerProvider { + + override def checkCustomerNumberAvailable(bankId : BankId, customerNumber : String) : Boolean = { + val customers = MappedCustomer.findAll( + By(MappedCustomer.mBank, bankId.value), + By(MappedCustomer.mNumber, customerNumber) + ) + + val available: Boolean = customers.size match { + case 0 => true + case _ => false + } + + available + } + override def getCustomer(bankId : BankId, user: User): Box[Customer] = { MappedCustomer.find( By(MappedCustomer.mUser, user.apiId.value), @@ -52,6 +67,8 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with def getSingleton = MappedCustomer + object mCustomerId extends MappedUUID(this) + object mUser extends MappedLongForeignKey(this, APIUser) object mBank extends DefaultStringField(this) @@ -69,6 +86,7 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with object mKycStatus extends MappedBoolean(this) object mLastOkDate extends MappedDateTime(this) + override def customerId: String = mCustomerId.get // id.toString override def number: String = mNumber.get override def mobileNumber: String = mMobileNumber.get override def legalName: String = mLegalName.get diff --git a/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala b/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala index 325e58dac..4ccf906ce 100644 --- a/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala +++ b/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala @@ -9,6 +9,7 @@ import net.liftweb.mapper._ object MappedKycDocumentsProvider extends KycDocumentProvider { + // TODO Add bankId (customerNumber is not unique) override def getKycDocuments(customerNumber: String): List[MappedKycDocument] = { MappedKycDocument.findAll( By(MappedKycDocument.mCustomerNumber, customerNumber), diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index b80e0b4ca..0616cdf94 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -112,6 +112,16 @@ object BankId { def unapply(id : String) = Some(BankId(id)) } + +case class CustomerId(val value : String) { + override def toString = value +} + +object CustomerId { + def unapply(id : String) = Some(CustomerId(id)) +} + + // In preparation for use in Context (api links) To replace OtherAccountId case class CounterpartyId(val value : String) { override def toString = value diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index cd9914747..afbf795ce 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -16,7 +16,7 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { case class MockFaceImage(date : Date, url : String) extends CustomerFaceImage - case class MockCustomer(number: String, mobileNumber: String, + case class MockCustomer(customerId: String, number: String, mobileNumber: String, legalName: String, email: String, faceImage: MockFaceImage, dateOfBirth: Date, relationshipStatus: String, dependents: Int, @@ -25,9 +25,12 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { val format = new java.text.SimpleDateFormat("dd/MM/yyyy") val mockCustomerFaceImage = MockFaceImage(new Date(1234000), "http://example.com/image1") - val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(format.parse("30/03/2012"), format.parse("30/03/2012"), format.parse("30/03/2014")), "Bachelor’s Degree", "Employed", true, new Date(1234000)) + val mockCustomer = MockCustomer("uuid-aisuhuiuhuikjhfd", "123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(format.parse("30/03/2012"), format.parse("30/03/2012"), format.parse("30/03/2014")), "Bachelor’s Degree", "Employed", true, new Date(1234000)) object MockedCustomerProvider extends CustomerProvider { + + override def checkCustomerNumberAvailable(bankId : BankId, customerNumber : String) = {true} + override def getCustomer(bankId: BankId, user: User): Box[Customer] = { if(bankId == mockBankId) Full(mockCustomer) else Empty @@ -105,7 +108,9 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { And("We should get the right information back") val info = response.body.extract[CustomerJson] - val received = MockCustomer(info.customer_number, + val received = MockCustomer( + info.customer_id, + info.customer_number, info.mobile_phone_number, info.legal_name, info.email, diff --git a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala index 4020e79c3..2f7726d04 100644 --- a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala +++ b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala @@ -17,6 +17,8 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { val mockBankId = BankId("testBank1") val mockCustomerNumber = "9393490320" + val mockCustomerId = "uuid-asdfasdfaoiu8u8u8hkjhsf" + val exampleDateString : String ="22/08/2013" val simpleDateFormat : SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") @@ -44,6 +46,7 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { //first add a customer to send message to var request = (v1_4Request / "banks" / mockBankId.value / "customer").POST <@ user1 var customerJson = CustomerJson( + customer_id = mockCustomerId, customer_number = mockCustomerNumber, legal_name = "Someone", mobile_phone_number = "125245", From 7498213a5e7a739bc782d7c834318671cab8d443 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 21 May 2016 15:16:36 +0200 Subject: [PATCH 462/702] Enabling createUser endpoint. CreateUserJSON tweaks --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 8 ++++---- src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala | 10 +++++----- src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 08024574e..e50d282bf 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1277,13 +1277,13 @@ trait APIMethods200 { | Optionally (Default False) require validation of email address. | |""", - emptyObjectJson, + Extraction.decompose(CreateUserJSON("someone@example.com", "my-secure-password", "James", "Brown")), emptyObjectJson, emptyObjectJson :: Nil, true, true, true, - List(apiTagPayment)) + List()) lazy val createUser: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "users" :: Nil JsonPost json -> _ => { @@ -1293,8 +1293,8 @@ trait APIMethods200 { } yield { if (OBPUser.find(By(OBPUser.email, postedData.email)).isEmpty) { val userCreated:Boolean = OBPUser.create - .firstName(postedData.firstName) - .lastName(postedData.lastName) + .firstName(postedData.first_name) + .lastName(postedData.last_name) .email(postedData.email) .password(postedData.password) .validated(false) diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index c46e45de0..551c86e34 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -68,11 +68,11 @@ class ResultAndLinksJSON( val links: LinksJSON ) -class CreateUserJSON( - val email: String, - val password: String, - val lastName: String, - val firstName: String +case class CreateUserJSON( + email: String, + password: String, + first_name: String, + last_name: String ) class BasicViewJSON( diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 4e6084246..d8d6ed2d8 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -163,8 +163,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getCoreAccountById, Implementations2_0_0.getCoreTransactionsForBankAccount, Implementations2_0_0.createAccount, - Implementations2_0_0.getTransactionTypes - + Implementations2_0_0.getTransactionTypes, + Implementations2_0_0.createUser //Implementations2_0_0.testLinks ) From abf01ebc32cf58c80cabb29c00d56fa6f20ef91b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 21 May 2016 22:18:55 +0200 Subject: [PATCH 463/702] Disable git-commit-id-plugin (2) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bd7c8f6ea..03c976312 100644 --- a/pom.xml +++ b/pom.xml @@ -352,7 +352,7 @@ - + From 69a423ba25fd4c02beeaab0071445e14f1f681a3 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 22 May 2016 03:09:17 +0200 Subject: [PATCH 464/702] Added createMeeting --- .../resources/props/sample.props.template | 6 ++ src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 51 +++++++++++ .../code/api/v2_0_0/JSONFactory2.0.0.scala | 45 +++++++++- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 4 +- .../code/meetings/MappedMeetingProvider.scala | 87 +++++++++++++++++++ src/main/scala/code/meetings/Meeting.scala | 51 +++++++++++ 7 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 src/main/scala/code/meetings/MappedMeetingProvider.scala create mode 100644 src/main/scala/code/meetings/Meeting.scala diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index e8d89e7e7..ee3d1ee8e 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -86,6 +86,12 @@ transactionRequests_enabled=true transactionRequests_connector=mapped +# For video conference meetings (createMeeting) +meeting.tokbox_enabled=false +meeting.tokbox_api_key=changeme +meeting.tokbox_api_secret=changeme + + ### management modules diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index f430147ec..f030311d4 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -46,6 +46,7 @@ import code.kycdocuments.MappedKycDocument import code.kycmedias.MappedKycMedia import code.kycchecks.MappedKycCheck import code.kycstatuses.MappedKycStatus +import code.meetings.MappedMeeting import code.socialmedia.MappedSocialMedia import code.management.{AccountsAPI, ImporterAPI} import code.metadata.comments.MappedComment @@ -415,5 +416,6 @@ object ToSchemify { MappedKycCheck, MappedKycStatus, MappedSocialMedia, - MappedTransactionType) + MappedTransactionType, + MappedMeeting) } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index e50d282bf..4ca774033 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1,8 +1,10 @@ package code.api.v2_0_0 import java.text.SimpleDateFormat +import java.util.Calendar import code.TransactionTypes.TransactionType +import code.api.APIFailure import code.api.util.APIUtil._ import code.api.util.ErrorMessages import code.api.v1_2_1.OBPAPI1_2_1._ @@ -23,6 +25,9 @@ import code.model._ import code.model.dataAccess.BankAccountCreation import code.socialmedia.SocialMediaHandle import code.transactionrequests.TransactionRequests + +import code.meetings.Meeting + import net.liftweb.common.{Full, _} import net.liftweb.http.rest.RestHelper import net.liftweb.http.{JsonResponse, Req} @@ -1311,6 +1316,52 @@ trait APIMethods200 { } } + + + resourceDocs += ResourceDoc( + createMeeting, + apiVersion, + "createMeeting", + "POST", + "/banks/BANK_ID/meetings", + "Initiate a meeting (video conference, call etc.) with the bank.", + "Currently this supports tokbox video conferences", + Extraction.decompose(CreateMeetingJSON("tokbox", "onboarding")), + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List()) + + + lazy val createMeeting: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "meetings" :: Nil JsonPost json -> _ => { + user => + if (Props.getBool("meeting.tokbox_enabled", false)) { + for { + // TODO use these keys to get session and tokens from tokbox + providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure("Meeting provider API Key is not configured.", 403) + providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure("Meeting provider Secret is not configured", 403) + u <- user ?~ ErrorMessages.UserNotLoggedIn + bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + postedData <- tryo {json.extract[CreateMeetingJSON]} ?~ ErrorMessages.InvalidJsonFormat + now = Calendar.getInstance().getTime() + meeting <- Meeting.meetingProvider.vend.createMeeting(bank.bankId, u, u, postedData.provider_id, postedData.purpose_id, now) + } yield { + // Format the data as V2.0.0 json + val json = JSONFactory200.createMeetingJSON(meeting) + successJsonResponse(Extraction.decompose(json), 201) + } + } else { + Full(errorJsonResponse("Sorry. createMeeting is not supported on this server.")) + } + } + } + + + + } } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 551c86e34..737dea669 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -35,6 +35,7 @@ import java.net.URL import java.util.Date import code.TransactionTypes.TransactionType.TransactionType +import code.meetings.Meeting import code.transactionrequests.TransactionRequests._ // import code.api.util.APIUtil.ApiLink @@ -75,6 +76,37 @@ case class CreateUserJSON( last_name: String ) + +case class CreateMeetingJSON( + provider_id: String, + purpose_id: String +) + +case class MeetingJSON( + meeting_id : String, + provider_id: String, + purpose_id: String, + bank_id : String, + present : MeetingPresentJSON, + keys : MeetingKeysJSON, + when : Date + ) + + +case class MeetingKeysJSON( + session_id: String, + customer_token: String, + staff_token: String + ) + +case class MeetingPresentJSON( + customer_user_id: String, + staff_user_id: String + ) + + + + class BasicViewJSON( val id: String, val short_name: String, @@ -624,11 +656,20 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy TransactionRequestWithChargeJSONs(trs.map(createTransactionRequestWithChargeJSON)) } + def createMeetingJSON(meeting : Meeting) : MeetingJSON = { + MeetingJSON(meeting_id = meeting.meetingId, + provider_id = meeting.providerId, + purpose_id = meeting.purposeId, + bank_id = meeting.bankId, + present = MeetingPresentJSON(staff_user_id = meeting.present.staffUserId, customer_user_id = meeting.present.customerUserId), + keys = MeetingKeysJSON(session_id = meeting.keys.sessionId, staff_token = meeting.keys.staffToken, customer_token = meeting.keys.customerToken), + when = meeting.when) + + } - - + // Copied from 1.2.1 (import just this def instead?) def stringOrNull(text : String) = diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index d8d6ed2d8..5afeaf0ab 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -164,8 +164,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getCoreTransactionsForBankAccount, Implementations2_0_0.createAccount, Implementations2_0_0.getTransactionTypes, - Implementations2_0_0.createUser - //Implementations2_0_0.testLinks + Implementations2_0_0.createUser, + Implementations2_0_0.createMeeting ) routes.foreach(route => { diff --git a/src/main/scala/code/meetings/MappedMeetingProvider.scala b/src/main/scala/code/meetings/MappedMeetingProvider.scala new file mode 100644 index 000000000..cd461458d --- /dev/null +++ b/src/main/scala/code/meetings/MappedMeetingProvider.scala @@ -0,0 +1,87 @@ +package code.meetings + +import java.util.Date + +import code.model.{BankId, User} +import code.model.dataAccess.APIUser +import code.util.{MappedUUID, DefaultStringField} +import net.liftweb.common.Box +import net.liftweb.mapper._ + + +object MappedMeetingProvider extends MeetingProvider { + + override def getMeetings(bankId : BankId, providerId : String, purposeId : String): Box[Meeting] = { + MappedMeeting.find( + By(MappedMeeting.mProviderId, providerId), + By(MappedMeeting.mPurposeId, purposeId)) + } + + + + override def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date) : Box[Meeting] = { + + val createdMeeting = MappedMeeting.create + .mBankId(bankId.value.toString) + .mStaffUserId(staffUser.apiId.value) + .mCustomerUserId(customerUser.apiId.value) + .mProviderId(providerId) + .mPurposeId(purposeId) + .mWhen(when) + .mSessionId("abcd") + .mCustomerToken("cust-token-asdlkfjlaksdjf") + .mStaffToken("staff-tok-asdlfkjlkjflkj") + .saveMe() + + Some(createdMeeting) + } + +} + + + + + +class MappedMeeting extends Meeting with LongKeyedMapper[MappedMeeting] with IdPK with CreatedUpdated { + + def getSingleton = MappedMeeting + + // Name the objects m* so that we can give the overriden methods nice names. + // Assume we'll have to override all fields so name them all m* + + object mMeetingId extends MappedUUID(this) + + // With + object mBankId extends DefaultStringField(this) + object mCustomerUserId extends MappedLongForeignKey(this, APIUser) + object mStaffUserId extends MappedLongForeignKey(this, APIUser) + + // What + object mProviderId extends DefaultStringField(this) + object mPurposeId extends DefaultStringField(this) + + // Keys to the "meeting room" + object mSessionId extends DefaultStringField(this) + object mCustomerToken extends DefaultStringField(this) + object mStaffToken extends DefaultStringField(this) + + object mWhen extends MappedDateTime(this) + + override def meetingId: String = mMeetingId.get.toString + + override def when: Date = mWhen.get + + override def providerId : String = mProviderId.get + override def purposeId : String = mPurposeId.get + override def bankId : String = mBankId.get.toString + + override def keys = MeetingKeys(mSessionId, mCustomerToken, mStaffToken) + override def present = MeetingPresent(mCustomerUserId.get.toString, mStaffUserId.get.toString) + + +} + +object MappedMeeting extends MappedMeeting with LongKeyedMetaMapper[MappedMeeting] { + //one Meeting info per bank for each api user + override def dbIndexes = UniqueIndex(mMeetingId) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/meetings/Meeting.scala b/src/main/scala/code/meetings/Meeting.scala new file mode 100644 index 000000000..49e9f9312 --- /dev/null +++ b/src/main/scala/code/meetings/Meeting.scala @@ -0,0 +1,51 @@ +package code.meetings + +import java.util.Date + +import code.model.{User, BankId} +import net.liftweb.common.Box +import net.liftweb.util.SimpleInjector + + +trait Meeting { + def meetingId: String + def providerId: String + def purposeId: String + def bankId: String + def present: MeetingPresent + def keys: MeetingKeys + def when: Date +} + + +case class MeetingKeys ( + sessionId: String, + customerToken: String, + staffToken: String + ) + +case class MeetingPresent( + staffUserId: String, + customerUserId: String + ) + + +object Meeting extends SimpleInjector { + + val meetingProvider = new Inject(buildOne _) {} + + def buildOne: MeetingProvider = MappedMeetingProvider + +} + +trait MeetingProvider { + def getMeetings(bankId : BankId, providerId: String, purposeId: String) : Box[Meeting] + + + def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date): Box[Meeting] + +} + + + + From 8516775be6da1607340304182ea5c508b90cdb9b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 22 May 2016 14:01:37 +0200 Subject: [PATCH 465/702] Added getMeetings, added Tags and ErrorMessage for createMeeting --- src/main/scala/code/api/util/APIUtil.scala | 8 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 83 +++++++++++++++++-- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 10 ++- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- .../code/meetings/MappedMeetingProvider.scala | 9 +- src/main/scala/code/meetings/Meeting.scala | 4 +- 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index eff4bc697..aea099794 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -32,7 +32,6 @@ package code.api.util - import code.api.Constant._ import code.api.v1_2.ErrorMessage import code.metrics.APIMetrics @@ -86,6 +85,10 @@ object ErrorMessages { val CustomerNumberAlreadyExists = "OBP-30006: Customer Number already exists. Please specify a different value for BANK_ID or CUSTOMER_NUMBER." val CustomerAlreadyExistsForUser = "OBP-30007: The User is already linked to a Customer at BANK_ID" + val MeetingsNotSupported = "OBP-30101: Meetings are not supported on this server." + val MeetingApiKeyNotConfigured = "OBP-30102: Meeting provider API Key is not configured." + val MeetingApiSecretNotConfigured = "OBP-30103: Meeting provider Secret is not configured." + // Transaction related messages: val InvalidTransactionRequestType = "OBP-40001: Invalid value for TRANSACTION_REQUEST_TYPE" @@ -346,6 +349,9 @@ object APIUtil extends Loggable { val apiTagCounterparties = ResourceDocTag("Counterparties") val apiTagKyc = ResourceDocTag("KYC") val apiTagCustomer = ResourceDocTag("Customer") + val apiTagOnboarding = ResourceDocTag("Onboarding") + val apiTagUser = ResourceDocTag("User") + val apiTagMeeting = ResourceDocTag("Meeting") // Used to document the API calls diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 4ca774033..32f6d3cd7 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1279,7 +1279,7 @@ trait APIMethods200 { | | Returns 409 error if username not unique. | - | Optionally (Default False) require validation of email address. + | May require validation of email address. | |""", Extraction.decompose(CreateUserJSON("someone@example.com", "my-secure-password", "James", "Brown")), @@ -1288,7 +1288,7 @@ trait APIMethods200 { true, true, true, - List()) + List(apiTagOnboarding, apiTagUser)) lazy val createUser: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "users" :: Nil JsonPost json -> _ => { @@ -1302,7 +1302,7 @@ trait APIMethods200 { .lastName(postedData.last_name) .email(postedData.email) .password(postedData.password) - .validated(false) + .validated(false) // TODO Get this from Props .save if (userCreated) successJsonResponse(JsRaw("{}"), 201) @@ -1324,15 +1324,28 @@ trait APIMethods200 { "createMeeting", "POST", "/banks/BANK_ID/meetings", - "Initiate a meeting (video conference, call etc.) with the bank.", - "Currently this supports tokbox video conferences", + "Create Meeting: Initiate a video conference/call with the bank.", + """The Meetings resource contains meta data about video/other conference sessions, not the video/audio/chat itself. + | + |The actual conferencing is handled by external providers. Currently OBP supports tokbox video conferences (WIP). + | + |This is not a recomendation of tokbox per se. + | + |provider_id determines the provider of the meeting / video chat service. MUST be url friendly (no spaces). + | + |purpose_id explains the purpose of the chat. onboarding | mortgage | complaint etc. MUST be url friendly (no spaces). + | + |Login is required. + | + |This call is experimental. Currently staff_user_id is not set. Further calls will be needed to correctly set this. + """.stripMargin, Extraction.decompose(CreateMeetingJSON("tokbox", "onboarding")), emptyObjectJson, emptyObjectJson :: Nil, true, true, true, - List()) + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser)) lazy val createMeeting: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1341,8 +1354,8 @@ trait APIMethods200 { if (Props.getBool("meeting.tokbox_enabled", false)) { for { // TODO use these keys to get session and tokens from tokbox - providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure("Meeting provider API Key is not configured.", 403) - providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure("Meeting provider Secret is not configured", 403) + providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) + providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) u <- user ?~ ErrorMessages.UserNotLoggedIn bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} postedData <- tryo {json.extract[CreateMeetingJSON]} ?~ ErrorMessages.InvalidJsonFormat @@ -1354,12 +1367,64 @@ trait APIMethods200 { successJsonResponse(Extraction.decompose(json), 201) } } else { - Full(errorJsonResponse("Sorry. createMeeting is not supported on this server.")) + Full(errorJsonResponse(ErrorMessages.MeetingsNotSupported)) } } } + resourceDocs += ResourceDoc( + getMeetings, + apiVersion, + "getMeetings", + "GET", + "/banks/BANK_ID/meetings", + "Get Meetings", + """Meetings contain meta data about, and are used to facilitate, video conferences / chats etc. + | + |The actual conference/chats are handled by external services. + | + |Login is required. + | + |This call is experimental and will require further authorisation in the future. + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser)) + + + lazy val getMeetings: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "meetings" :: Nil JsonGet _ => { + user => + if (Props.getBool("meeting.tokbox_enabled", false)) { + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + + providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) + providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) + u <- user ?~ ErrorMessages.UserNotLoggedIn + bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + // now = Calendar.getInstance().getTime() + meetings <- Meeting.meetingProvider.vend.getMeetings(bank.bankId, u) + } + yield { + // Format the data as V2.0.0 json + val json = JSONFactory200.createMeetingJSONs(meetings) + successJsonResponse(Extraction.decompose(json)) + } + } else { + Full(errorJsonResponse(ErrorMessages.MeetingsNotSupported)) + } + } + } + + + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 737dea669..60a5176f2 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -92,6 +92,10 @@ case class MeetingJSON( when : Date ) +case class MeetingJSONs( + meetings : List[MeetingJSON] + ) + case class MeetingKeysJSON( session_id: String, @@ -105,8 +109,6 @@ case class MeetingPresentJSON( ) - - class BasicViewJSON( val id: String, val short_name: String, @@ -667,9 +669,11 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy } + def createMeetingJSONs(meetings : List[Meeting]) : MeetingJSONs = { + MeetingJSONs(meetings.map(createMeetingJSON)) + } - // Copied from 1.2.1 (import just this def instead?) def stringOrNull(text : String) = diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 5afeaf0ab..6b50a04b2 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -165,7 +165,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.createAccount, Implementations2_0_0.getTransactionTypes, Implementations2_0_0.createUser, - Implementations2_0_0.createMeeting + Implementations2_0_0.createMeeting, + Implementations2_0_0.getMeetings ) routes.foreach(route => { diff --git a/src/main/scala/code/meetings/MappedMeetingProvider.scala b/src/main/scala/code/meetings/MappedMeetingProvider.scala index cd461458d..65dfa73b4 100644 --- a/src/main/scala/code/meetings/MappedMeetingProvider.scala +++ b/src/main/scala/code/meetings/MappedMeetingProvider.scala @@ -11,10 +11,9 @@ import net.liftweb.mapper._ object MappedMeetingProvider extends MeetingProvider { - override def getMeetings(bankId : BankId, providerId : String, purposeId : String): Box[Meeting] = { - MappedMeeting.find( - By(MappedMeeting.mProviderId, providerId), - By(MappedMeeting.mPurposeId, purposeId)) + override def getMeetings(bankId : BankId, userId: User): Box[List[Meeting]] = { + // Return a Box so we can handle errors later. + Some(MappedMeeting.findAll()) } @@ -23,7 +22,7 @@ object MappedMeetingProvider extends MeetingProvider { val createdMeeting = MappedMeeting.create .mBankId(bankId.value.toString) - .mStaffUserId(staffUser.apiId.value) + //.mStaffUserId(staffUser.apiId.value) .mCustomerUserId(customerUser.apiId.value) .mProviderId(providerId) .mPurposeId(purposeId) diff --git a/src/main/scala/code/meetings/Meeting.scala b/src/main/scala/code/meetings/Meeting.scala index 49e9f9312..92085b6b1 100644 --- a/src/main/scala/code/meetings/Meeting.scala +++ b/src/main/scala/code/meetings/Meeting.scala @@ -39,9 +39,7 @@ object Meeting extends SimpleInjector { } trait MeetingProvider { - def getMeetings(bankId : BankId, providerId: String, purposeId: String) : Box[Meeting] - - + def getMeetings(bankId : BankId, userId: User) : Box[List[Meeting]] def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date): Box[Meeting] } From 68d03e92ad9a0f7e5992cafbb32558d6373da2ac Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 23 May 2016 09:27:36 +0200 Subject: [PATCH 466/702] Switch from scala compiler 2.11.7. to 2.11.8 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 03c976312..6bb83f999 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ 2011 2.11 - 2.11.7 + 2.11.8 2.6.3 UTF-8 From 2bdfb06c3153d44e18affe877d04e20617db5208 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 23 May 2016 16:47:19 +0200 Subject: [PATCH 467/702] Create end point for meetings #37 - calls tokbox added --- pom.xml | 5 +++++ src/main/scala/code/api/v2_0_0/APIMethods200.scala | 5 ++++- .../scala/code/meetings/MappedMeetingProvider.scala | 10 +++++----- src/main/scala/code/meetings/Meeting.scala | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 6bb83f999..fcf927db5 100644 --- a/pom.xml +++ b/pom.xml @@ -176,6 +176,11 @@ authentikat-jwt_${scala.version} 0.4.1 + + com.tokbox + opentok-server-sdk + 3.0.0-beta.1 + diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 32f6d3cd7..c52df770e 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1360,7 +1360,10 @@ trait APIMethods200 { bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} postedData <- tryo {json.extract[CreateMeetingJSON]} ?~ ErrorMessages.InvalidJsonFormat now = Calendar.getInstance().getTime() - meeting <- Meeting.meetingProvider.vend.createMeeting(bank.bankId, u, u, postedData.provider_id, postedData.purpose_id, now) + sessionId <- tryo{code.opentok.OpenTokUtil.getSession.getSessionId()} + customerToken <- tryo{code.opentok.OpenTokUtil.generateTokenForPublisher(60)} + staffToken <- tryo{code.opentok.OpenTokUtil.generateTokenForModerator(60)} + meeting <- Meeting.meetingProvider.vend.createMeeting(bank.bankId, u, u, postedData.provider_id, postedData.purpose_id, now, sessionId, customerToken, staffToken) } yield { // Format the data as V2.0.0 json val json = JSONFactory200.createMeetingJSON(meeting) diff --git a/src/main/scala/code/meetings/MappedMeetingProvider.scala b/src/main/scala/code/meetings/MappedMeetingProvider.scala index 65dfa73b4..e7d2b5e57 100644 --- a/src/main/scala/code/meetings/MappedMeetingProvider.scala +++ b/src/main/scala/code/meetings/MappedMeetingProvider.scala @@ -13,12 +13,12 @@ object MappedMeetingProvider extends MeetingProvider { override def getMeetings(bankId : BankId, userId: User): Box[List[Meeting]] = { // Return a Box so we can handle errors later. - Some(MappedMeeting.findAll()) + Some(MappedMeeting.findAll(By(MappedMeeting.mBankId, bankId.toString), OrderBy(MappedMeeting.mWhen, Descending))) } - override def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date) : Box[Meeting] = { + override def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date, sessionId: String, customerToken: String, staffToken: String) : Box[Meeting] = { val createdMeeting = MappedMeeting.create .mBankId(bankId.value.toString) @@ -27,9 +27,9 @@ object MappedMeetingProvider extends MeetingProvider { .mProviderId(providerId) .mPurposeId(purposeId) .mWhen(when) - .mSessionId("abcd") - .mCustomerToken("cust-token-asdlkfjlaksdjf") - .mStaffToken("staff-tok-asdlfkjlkjflkj") + .mSessionId(sessionId) + .mCustomerToken(customerToken) + .mStaffToken(staffToken) .saveMe() Some(createdMeeting) diff --git a/src/main/scala/code/meetings/Meeting.scala b/src/main/scala/code/meetings/Meeting.scala index 92085b6b1..09836582b 100644 --- a/src/main/scala/code/meetings/Meeting.scala +++ b/src/main/scala/code/meetings/Meeting.scala @@ -40,7 +40,7 @@ object Meeting extends SimpleInjector { trait MeetingProvider { def getMeetings(bankId : BankId, userId: User) : Box[List[Meeting]] - def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date): Box[Meeting] + def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date, sessionId: String, customerToken: String, staffToken: String): Box[Meeting] } From 0f8c696412a7b781538e7b85f11707809c86d85d Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 23 May 2016 17:10:12 +0200 Subject: [PATCH 468/702] Create end point for meetings #37 - calls tokbox added --- src/main/java/code/opentok/OpenTokUtil.java | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/main/java/code/opentok/OpenTokUtil.java diff --git a/src/main/java/code/opentok/OpenTokUtil.java b/src/main/java/code/opentok/OpenTokUtil.java new file mode 100644 index 000000000..3154284b5 --- /dev/null +++ b/src/main/java/code/opentok/OpenTokUtil.java @@ -0,0 +1,67 @@ +package code.opentok; + + +import com.opentok.OpenTok; +import com.opentok.Session; +import com.opentok.TokenOptions; +import com.opentok.Role; +import net.liftweb.util.Props; +import com.opentok.exception.OpenTokException; +import com.opentok.MediaMode; +import com.opentok.SessionProperties; + +/** + * Created by markom on 5/22/16. + */ +public class OpenTokUtil extends Exception { + private static Session session; + + public OpenTokUtil() { + // Empty constructor + } + + public static OpenTok createOpenTok() { + // Set the following constants with the API key and API secret + // that you receive when you sign up to use the OpenTok API: + int apiKey = Integer.parseInt(Props.get("meeting.tokbox_api_key", "0000")); + String apiSecret = Props.get("meeting.tokbox_api_secret", "YOUR API SECRET"); + OpenTok opentok = new OpenTok(apiKey, apiSecret); + return opentok; + } + + public static Session getSession() throws OpenTokException { + if(session == null){ + // A session that uses the OpenTok Media Router: + session = createOpenTok().createSession(new SessionProperties.Builder() + .mediaMode(MediaMode.ROUTED) + .build()); + } + return session; + } + + public static String generateTokenForModerator(int expireTimeInMinutes) throws OpenTokException { + + // Generate a token. Use the Role MODERATOR. Expire time is defined by parameter expireTimeInMinutes. + String token = session.generateToken(new TokenOptions.Builder() + .role(Role.MODERATOR) + .expireTime((System.currentTimeMillis() / 1000L) + (expireTimeInMinutes * 60)) // in expireTimeInMinutes + .data("name=Simon") + .build()); + + return token; + } + + public static String generateTokenForPublisher(int expireTimeInMinutes) throws OpenTokException { + + // Generate a token. Use the Role PUBLISHER. Expire time is defined by parameter expireTimeInMinutes. + String token = session.generateToken(new TokenOptions.Builder() + .role(Role.PUBLISHER) + .expireTime((System.currentTimeMillis() / 1000L) + (expireTimeInMinutes * 60)) // in expireTimeInMinutes + .data("name=Simon") + .build()); + + return token; + } + + +} From 066901fa7b08555794c1b1cb59ca0fab91c94a6e Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 23 May 2016 23:55:57 +0200 Subject: [PATCH 469/702] Adding getMeeting. Fixed staff_user_id / customer_user_id swap. changed order of staff_token, customer_token in json (cosmetic) --- .../scala/code/api/v2_0_0/APIMethods200.scala | 52 ++++++++++++++++++- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 16 +++--- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- .../code/meetings/MappedMeetingProvider.scala | 19 ++++++- src/main/scala/code/meetings/Meeting.scala | 2 +- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index c52df770e..d6282ae69 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1337,7 +1337,7 @@ trait APIMethods200 { | |Login is required. | - |This call is experimental. Currently staff_user_id is not set. Further calls will be needed to correctly set this. + |This call is *experimental*. Currently staff_user_id is not set. Further calls will be needed to correctly set this. """.stripMargin, Extraction.decompose(CreateMeetingJSON("tokbox", "onboarding")), emptyObjectJson, @@ -1389,7 +1389,7 @@ trait APIMethods200 { | |Login is required. | - |This call is experimental and will require further authorisation in the future. + |This call is *experimental* and will require further authorisation in the future. """.stripMargin, emptyObjectJson, emptyObjectJson, @@ -1428,6 +1428,54 @@ trait APIMethods200 { + resourceDocs += ResourceDoc( + getMeeting, + apiVersion, + "getMeeting", + "GET", + "/banks/BANK_ID/meetings/MEETING_ID", + "Get Meeting specified by BANK_ID / MEETING_ID", + """Meetings contain meta data about, and are used to facilitate, video conferences / chats etc. + | + |The actual conference/chats are handled by external services. + | + |Login is required. + | + |This call is *experimental* and will require further authorisation in the future. + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser)) + + + lazy val getMeeting: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "meetings" :: meetingId :: Nil JsonGet _ => { + user => + if (Props.getBool("meeting.tokbox_enabled", false)) { + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) + providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) + u <- user ?~ ErrorMessages.UserNotLoggedIn + bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + meeting <- Meeting.meetingProvider.vend.getMeeting(bank.bankId, u, meetingId) + } + yield { + // Format the data as V2.0.0 json + val json = JSONFactory200.createMeetingJSON(meeting) + successJsonResponse(Extraction.decompose(json)) + } + } else { + Full(errorJsonResponse(ErrorMessages.MeetingsNotSupported)) + } + } + } + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 60a5176f2..5cf193437 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -99,13 +99,14 @@ case class MeetingJSONs( case class MeetingKeysJSON( session_id: String, - customer_token: String, - staff_token: String + staff_token: String, + customer_token: String ) case class MeetingPresentJSON( - customer_user_id: String, - staff_user_id: String + staff_user_id: String, + customer_user_id: String + ) @@ -663,8 +664,11 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy provider_id = meeting.providerId, purpose_id = meeting.purposeId, bank_id = meeting.bankId, - present = MeetingPresentJSON(staff_user_id = meeting.present.staffUserId, customer_user_id = meeting.present.customerUserId), - keys = MeetingKeysJSON(session_id = meeting.keys.sessionId, staff_token = meeting.keys.staffToken, customer_token = meeting.keys.customerToken), + present = MeetingPresentJSON(staff_user_id = meeting.present.staffUserId, + customer_user_id = meeting.present.customerUserId), + keys = MeetingKeysJSON(session_id = meeting.keys.sessionId, + staff_token = meeting.keys.staffToken, + customer_token = meeting.keys.customerToken), when = meeting.when) } diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 6b50a04b2..2b3be80f0 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -166,7 +166,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getTransactionTypes, Implementations2_0_0.createUser, Implementations2_0_0.createMeeting, - Implementations2_0_0.getMeetings + Implementations2_0_0.getMeetings, + Implementations2_0_0.getMeeting ) routes.foreach(route => { diff --git a/src/main/scala/code/meetings/MappedMeetingProvider.scala b/src/main/scala/code/meetings/MappedMeetingProvider.scala index e7d2b5e57..4e79c1262 100644 --- a/src/main/scala/code/meetings/MappedMeetingProvider.scala +++ b/src/main/scala/code/meetings/MappedMeetingProvider.scala @@ -11,9 +11,23 @@ import net.liftweb.mapper._ object MappedMeetingProvider extends MeetingProvider { + + override def getMeeting(bankId : BankId, userId: User, meetingId : String): Box[Meeting] = { + // Return a Box so we can handle errors later. + MappedMeeting.find( + // TODO Need to check permissions (user) + By(MappedMeeting.mBankId, bankId.toString), + By(MappedMeeting.mMeetingId, meetingId) + , OrderBy(MappedMeeting.mWhen, Descending)) + } + + override def getMeetings(bankId : BankId, userId: User): Box[List[Meeting]] = { // Return a Box so we can handle errors later. - Some(MappedMeeting.findAll(By(MappedMeeting.mBankId, bankId.toString), OrderBy(MappedMeeting.mWhen, Descending))) + Some(MappedMeeting.findAll(By( + // TODO Need to check permissions (user) + MappedMeeting.mBankId, bankId.toString), + OrderBy(MappedMeeting.mWhen, Descending))) } @@ -75,7 +89,8 @@ class MappedMeeting extends Meeting with LongKeyedMapper[MappedMeeting] with IdP override def bankId : String = mBankId.get.toString override def keys = MeetingKeys(mSessionId, mCustomerToken, mStaffToken) - override def present = MeetingPresent(mCustomerUserId.get.toString, mStaffUserId.get.toString) + override def present = MeetingPresent(staffUserId = mStaffUserId.get.toString, + customerUserId = mCustomerUserId.get.toString) } diff --git a/src/main/scala/code/meetings/Meeting.scala b/src/main/scala/code/meetings/Meeting.scala index 09836582b..da5f32385 100644 --- a/src/main/scala/code/meetings/Meeting.scala +++ b/src/main/scala/code/meetings/Meeting.scala @@ -41,7 +41,7 @@ object Meeting extends SimpleInjector { trait MeetingProvider { def getMeetings(bankId : BankId, userId: User) : Box[List[Meeting]] def createMeeting(bankId: BankId, staffUser: User, customerUser : User, providerId : String, purposeId : String, when: Date, sessionId: String, customerToken: String, staffToken: String): Box[Meeting] - + def getMeeting(bankId : BankId, userId: User, meetingId : String) : Box[Meeting] } From 03424eb7b2cb39a7d847cb942cb762679faf65e8 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 24 May 2016 00:03:21 +0200 Subject: [PATCH 470/702] Added ApiTagExperimental and added to Meetings related calls --- src/main/scala/code/api/util/APIUtil.scala | 1 + src/main/scala/code/api/v2_0_0/APIMethods200.scala | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index aea099794..602afbd0c 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -352,6 +352,7 @@ object APIUtil extends Loggable { val apiTagOnboarding = ResourceDocTag("Onboarding") val apiTagUser = ResourceDocTag("User") val apiTagMeeting = ResourceDocTag("Meeting") + val apiTagExperimental = ResourceDocTag("Experimental!") // Used to document the API calls diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index d6282ae69..667f89af5 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1337,7 +1337,7 @@ trait APIMethods200 { | |Login is required. | - |This call is *experimental*. Currently staff_user_id is not set. Further calls will be needed to correctly set this. + |This call is **experimental**. Currently staff_user_id is not set. Further calls will be needed to correctly set this. """.stripMargin, Extraction.decompose(CreateMeetingJSON("tokbox", "onboarding")), emptyObjectJson, @@ -1345,7 +1345,7 @@ trait APIMethods200 { true, true, true, - List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser)) + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) lazy val createMeeting: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1389,7 +1389,7 @@ trait APIMethods200 { | |Login is required. | - |This call is *experimental* and will require further authorisation in the future. + |This call is **experimental** and will require further authorisation in the future. """.stripMargin, emptyObjectJson, emptyObjectJson, @@ -1397,7 +1397,7 @@ trait APIMethods200 { true, true, true, - List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser)) + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) lazy val getMeetings: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1441,7 +1441,7 @@ trait APIMethods200 { | |Login is required. | - |This call is *experimental* and will require further authorisation in the future. + |This call is **experimental** and will require further authorisation in the future. """.stripMargin, emptyObjectJson, emptyObjectJson, @@ -1449,7 +1449,7 @@ trait APIMethods200 { true, true, true, - List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser)) + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) lazy val getMeeting: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { From eeca850295207be4c4026ebf88f15071d520f1a5 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 24 May 2016 15:52:09 +0200 Subject: [PATCH 471/702] Changing createUser to validated=true --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 667f89af5..f5c4715e7 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1302,7 +1302,7 @@ trait APIMethods200 { .lastName(postedData.last_name) .email(postedData.email) .password(postedData.password) - .validated(false) // TODO Get this from Props + .validated(true) // TODO Get this from Props .save if (userCreated) successJsonResponse(JsRaw("{}"), 201) From 0701ced4b1c642ee4091d2597ed0b05da871873c Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 24 May 2016 19:14:26 +0200 Subject: [PATCH 472/702] Added test for createUser and code cleanup --- .../bankconnectors/KafkaMappedConnector.scala | 9 +- .../scala/code/model/dataAccess/OBPUser.scala | 9 +- src/test/scala/code/api/directloginTest.scala | 3 +- .../code/api/v2_0_0/CreateUserTest.scala | 115 ++++++++++++++++++ .../api/v2_0_0/TransactionRequestsTest.scala | 8 +- 5 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 src/test/scala/code/api/v2_0_0/CreateUserTest.scala diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 76f24fec8..41e2b9691 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -99,7 +99,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable cachedUserAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) } val res = { - for (r <- rList) yield { + for (r <- rList if ! viewExists(r)) yield { val views = createSaveableViews(r) views.foreach(_.save()) views.map(_.value).filterNot(_.isPublic).foreach(v => { @@ -108,6 +108,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable setAccountOwner(apiUser.email.get, r) } } + res } def updatePublicAccountViews( user: APIUser ): List[List[Saveable[ViewType]]] = { @@ -121,7 +122,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable cachedPublicAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getPublicAccounts", argList).extract[List[KafkaInboundAccount]]) } val res = { - for (r <- rList) yield { + for (r <- rList if ! viewExists(r)) yield { val views = createSaveableViews(r) views.foreach(_.save()) views @@ -130,6 +131,10 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable res } + def viewExists(acc: KafkaInboundAccount): Boolean = { + Views.views.vend.permittedViews(User.findByApiId(acc.id.toLong).orNull, acc.asInstanceOf[BankAccount]).nonEmpty + } + def createSaveableViews(acc : KafkaInboundAccount) : List[Saveable[ViewType]] = { val bankId = BankId(acc.bank) val accountId = AccountId(acc.id) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 970532951..f37fa3491 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -173,9 +173,9 @@ import net.liftweb.util.Helpers._ passwordResetPath.mkString("/", "/", "/")+urlEncode(user.getUniqueId()) Mailer.sendMail(From(emailFrom),Subject(passwordResetEmailSubject), - (To(user.getEmail) :: + To(user.getEmail) :: generateResetEmailBodies(user, resetLink) ::: - (bccEmail.toList.map(BCC(_)))) :_*) + (bccEmail.toList.map(BCC(_))) :_*) S.notice(S.?("password.reset.email.sent")) S.redirectTo(homePage) @@ -230,9 +230,9 @@ import net.liftweb.util.Helpers._ val msgXml = signupMailBody(user, resetLink) Mailer.sendMail(From(emailFrom),Subject(signupMailSubject), - (To(user.getEmail) :: + To(user.getEmail) :: generateValidationEmailBodies(user, resetLink) ::: - (bccEmail.toList.map(BCC(_)))) :_* ) + (bccEmail.toList.map(BCC(_))) :_* ) } /** @@ -288,7 +288,6 @@ import net.liftweb.util.Helpers._ def getExternalUser(username: String, password: String):Box[OBPUser] = { KafkaMappedConnector.getUser(username, password) match { case Full(KafkaInboundUser(extEmail, extPassword, extDisplayName)) => { - val preLoginState = capturePreLoginState() info("external user authenticated. login redir: " + loginRedirect.get) val redir = loginRedirect.get match { case Full(url) => diff --git a/src/test/scala/code/api/directloginTest.scala b/src/test/scala/code/api/directloginTest.scala index b911f0344..3f397ee89 100644 --- a/src/test/scala/code/api/directloginTest.scala +++ b/src/test/scala/code/api/directloginTest.scala @@ -11,14 +11,13 @@ import org.scalatest.BeforeAndAfter -class directloginTest extends ServerSetup with BeforeAndAfter{ +class directloginTest extends ServerSetup with BeforeAndAfter { val KEY = randomString(40).toLowerCase val SECRET = randomString(40).toLowerCase val EMAIL = randomString(10).toLowerCase + "@example.com" val PASSWORD = randomString(20) - //def setupUserAndConsumer = { before { if (OBPUser.find(By(OBPUser.email, EMAIL)).isEmpty) OBPUser.create. diff --git a/src/test/scala/code/api/v2_0_0/CreateUserTest.scala b/src/test/scala/code/api/v2_0_0/CreateUserTest.scala new file mode 100644 index 000000000..39526763c --- /dev/null +++ b/src/test/scala/code/api/v2_0_0/CreateUserTest.scala @@ -0,0 +1,115 @@ +package code.api.v2_0_0 + +import code.api.OAuthResponse +import code.api.util.APIUtil.OAuth.{Consumer, _} +import code.model.{Consumer => OBPConsumer} +import dispatch._ +import net.liftweb.json.JsonAST.{JField, JObject, JString} +import net.liftweb.json.Serialization.write +import net.liftweb.util.Helpers._ +import org.scalatest.{BeforeAndAfter, Tag} + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.Duration + + +class CreateUserTest extends V200ServerSetup with BeforeAndAfter { + + object CreateUser extends Tag("createUser") + + val FIRSTNAME = randomString(8).toLowerCase + val LASTNAME = randomString(16).toLowerCase + val EMAIL = randomString(10).toLowerCase + "@example.com" + val PASSWORD = randomString(20) + + //setup Consumer + val KEY = randomString(40).toLowerCase + val SECRET = randomString(40).toLowerCase + + before { + val testConsumer = OBPConsumer.create. + name("test application"). + isActive(true). + key(KEY). + secret(SECRET). + saveMe + } + + val consumer = new Consumer(KEY, SECRET) + + def oauthRequest = baseRequest / "oauth" + def directLoginRequest = baseRequest / "my" / "logins" / "direct" + + val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") + val validHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". + format(EMAIL, PASSWORD, KEY)) + val validHeaders = List(accessControlOriginHeader, validHeader) + + private def getAPIResponse(req : Req) : OAuthResponse = { + Await.result( + for(response <- Http(req > as.Response(p => p))) + yield OAuthResponse(response.getStatusCode, response.getResponseBody), Duration.Inf) + } + + private def sendPostRequest(req: Req): OAuthResponse = { + val postReq = req.POST + getAPIResponse(postReq) + } + + private def getRequestToken(consumer: Consumer, callback: String): OAuthResponse = { + val request = (oauthRequest / "initiate").POST <@ (consumer, callback) + sendPostRequest(request) + } + + def extractToken(body: String) : Token = { + val oauthParams = body.split("&") + val token = oauthParams(0).split("=")(1) + val secret = oauthParams(1).split("=")(1) + Token(token, secret) + } + + + feature("we can create an user and login as newly created user using both directLogin and OAuth") { + + scenario("we create an user with email, first name, last name and password", CreateUser) { + When("we create a new user") + val params = Map("email" -> EMAIL, + "password" -> PASSWORD, + "first_name" -> FIRSTNAME, + "last_name" -> LASTNAME) + + var request = (v2_0Request / "users").POST + var response = makePostRequest(request, write(params)) + Then("we should get a 201 created code") + response.code should equal(201) + } + + scenario("we login using directLogin as newly created user", CreateUser) { + When("we request a directLogin token") + var request = directLoginRequest + var response = makePostRequestAdditionalHeader(request, "", validHeaders) + var token = "INVALID" + Then("we should get a 200 - OK and a token") + response.code should equal(200) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + token.size should not equal (0) + } + + scenario("we login using OAuth as newly created user", CreateUser) { + When("the request an OAuth token") + val reply = getRequestToken(consumer, oob) + Then("we should get a 200 - OK and a token") + reply.code should equal (200) + val requestToken = extractToken(reply.body) + requestToken.value.size should not equal (0) + } + + } +} \ No newline at end of file diff --git a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala index 40033ace3..0de719b86 100644 --- a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala @@ -29,7 +29,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // No challenge, No FX (same currencies) - if (Props.getBool("transactionRequests_enabled", false) == false) { + if (Props.getBool("transactionRequests_enabled", false)) { ignore("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) {} } else { scenario("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) { @@ -168,7 +168,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers } // No challenge, with FX - if (Props.getBool("transactionRequests_enabled", false) == false) { + if (Props.getBool("transactionRequests_enabled", false)) { ignore("we create an FX transaction request without challenge, with FX (different currencies)", TransactionRequest) {} } else { scenario("we create an FX transaction request without challenge, with FX (different currencies)", TransactionRequest) { @@ -380,7 +380,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // With challenge, No FX (Same currencies) - if (Props.getBool("transactionRequests_enabled", false) == false) { + if (Props.getBool("transactionRequests_enabled", false)) { ignore("we create a transaction request with a challenge, same currencies", TransactionRequest) {} } else { scenario("we create a transaction request with a challenge", TransactionRequest) { @@ -551,7 +551,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // With Challenge, with FX - if (Props.getBool("transactionRequests_enabled", false) == false) { + if (Props.getBool("transactionRequests_enabled", false)) { ignore("we create an FX transaction request with challenge", TransactionRequest) {} } else { scenario("we create an FX transaction request with challenge", TransactionRequest) { From 946114cb247a6febca60123de89f98e735193d26 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 24 May 2016 21:35:52 +0200 Subject: [PATCH 473/702] Added createCustomer in v2.0.0 - accepts a passed in user_id WIP --- src/main/scala/code/api/util/APIUtil.scala | 4 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 71 ++++++++++++++++++- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 19 ++++- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 602afbd0c..7671f080e 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -62,15 +62,17 @@ object ErrorMessages { - // Authentication / Authorisation messages + // Authentication / Authorisation / User messages val UserNotLoggedIn = "OBP-20001: User not logged in. Authentication is required!" + val DirectLoginMissingParameters = "OBP-20002: These DirectLogin parameters are missing: " val DirectLoginInvalidToken = "OBP-20003: This DirectLogin token is invalid or expired: " val InvalidLoginCredentials = "OBP-20004: Invalid login credentials. Check username/password." + val UserNotFoundById = "OBP-20005: User not found by User Id." // Resource related messages diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index f5c4715e7..23369a9af 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -9,7 +9,9 @@ import code.api.util.APIUtil._ import code.api.util.ErrorMessages import code.api.v1_2_1.OBPAPI1_2_1._ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121, JSONFactory => JSONFactory121} -import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJSON} +import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, ChallengeAnswerJSON, TransactionRequestAccountJSON} +//import code.api.v2_0_0.{CreateCustomerJson} + import code.model.dataAccess.OBPUser import net.liftweb.mapper.By @@ -38,7 +40,7 @@ import net.liftweb.util.Props import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer // Makes JValue assignment to Nil work -import code.customer.Customer +import code.customer.{MockCustomerFaceImage, Customer} import code.util.Helper._ import net.liftweb.http.js.JE.JsRaw import net.liftweb.json.Extraction @@ -1476,6 +1478,71 @@ trait APIMethods200 { } } + // + + resourceDocs += ResourceDoc( + createCustomer, + apiVersion, + "createCustomer", + "POST", + "/banks/BANK_ID/customers", + "Create Customer.", + """Add a customer linked to the user specified by user_id + |The Customer resource stores the customer number, legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc. + |This call may require additional permissions/role in the future. + |For now the authenticated user can create at most one linked customer. + |Dates need to be in the format 2013-01-21T23:08:00Z + |OAuth authentication is required. + |""", + Extraction.decompose(CreateCustomerJson("user_id to attach this customer to e.g. 123213", "new customer number 687687678", "Joe David Bloggs", + "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), + exampleDate, "Single", 1, List(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)), + emptyObjectJson, + emptyObjectJson :: Nil, + false, + false, + false, + List(apiTagCustomer)) + + lazy val createCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "customers" :: Nil JsonPost json -> _ => { + user => + for { + u <- user ?~! "User must be logged in to post Customer" // TODO. CHECK user has role to create a customer / create a customer for another user id. + bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + postedData <- tryo{json.extract[CreateCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat + checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists + // TODO The user id we expose should be a uuid . For now we have a long direct from the database. + user_id <- tryo {postedData.user_id.toLong} ?~ ErrorMessages.InvalidNumber + customer_user <- User.findByApiId(user_id) ?~! ErrorMessages.UserNotFoundById + customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, customer_user).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser + customer <- Customer.customerProvider.vend.addCustomer(bankId, + customer_user, + postedData.customer_number, + postedData.legal_name, + postedData.mobile_phone_number, + postedData.email, + MockCustomerFaceImage(postedData.face_image.date, postedData.face_image.url), + postedData.date_of_birth, + postedData.relationship_status, + postedData.dependants, + postedData.dob_of_dependants, + postedData.highest_education_attained, + postedData.employment_status, + postedData.kyc_status, + postedData.last_ok_date) ?~! "Could not create customer" + } yield { + val successJson = Extraction.decompose(customer) + successJsonResponse(successJson) + } + } + } + + + + + + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 5cf193437..06f96798b 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -41,7 +41,7 @@ import code.transactionrequests.TransactionRequests._ // import code.api.util.APIUtil.ApiLink import code.api.v1_2_1.{AmountOfMoneyJSON, JSONFactory => JSONFactory121, MinimalBankJSON => MinimalBankJSON121, OtherAccountJSON => OtherAccountJSON121, ThisAccountJSON => ThisAccountJSON121, TransactionDetailsJSON => TransactionDetailsJSON121, UserJSON => UserJSON121, ViewJSON => ViewJSON121} -import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeJSON, TransactionRequestAccountJSON} +import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, ChallengeJSON, TransactionRequestAccountJSON} import code.kycchecks.KycCheck import code.kycdocuments.KycDocument import code.kycmedias.KycMedia @@ -193,6 +193,23 @@ case class SocialMediaJSON( ) case class SocialMediasJSON(checks: List[SocialMediaJSON]) +case class CreateCustomerJson( + user_id: String, + customer_number : String, + legal_name : String, + mobile_phone_number : String, + email : String, + face_image : CustomerFaceImageJson, + date_of_birth: Date, + relationship_status: String, + dependants: Int, + dob_of_dependants: List[Date], + highest_education_attained: String, + employment_status: String, + kyc_status: Boolean, + last_ok_date: Date) + + // TODO Use the scala doc of a case class in the Resource Doc if a case class is given as a return type diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 2b3be80f0..9aa4c9a1f 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -167,7 +167,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.createUser, Implementations2_0_0.createMeeting, Implementations2_0_0.getMeetings, - Implementations2_0_0.getMeeting + Implementations2_0_0.getMeeting, + Implementations2_0_0.createCustomer ) routes.foreach(route => { From a39fe5506f00f1304e572aefb5e9f8c3d1ac4ece Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 24 May 2016 21:51:20 +0200 Subject: [PATCH 474/702] Fixed viewExists --- src/main/scala/code/bankconnectors/KafkaMappedConnector.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 41e2b9691..3e0e99378 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -132,7 +132,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def viewExists(acc: KafkaInboundAccount): Boolean = { - Views.views.vend.permittedViews(User.findByApiId(acc.id.toLong).orNull, acc.asInstanceOf[BankAccount]).nonEmpty + Views.views.vend.permittedViews(User.findByApiId(acc.id.toLong).orNull, getBankAccount(BankId(acc.bank), AccountId(acc.id)).orNull).nonEmpty } def createSaveableViews(acc : KafkaInboundAccount) : List[Saveable[ViewType]] = { @@ -919,7 +919,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), label = r.label, nationalIdentifier = r.nationalIdentifier, //TODO - swift_bic = Some(r.swift_bic.get), //TODO + swift_bic = Some(r.swift_bic.get), //TODO iban = Some(r.iban.get), number = r.number, bankName = r.bankName, From 7e87ee27ee3537317669dda32e59ea6e9ebd77b3 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 24 May 2016 22:09:46 +0200 Subject: [PATCH 475/702] Roolback changes that broke TransactionRequestsTest --- .../scala/code/api/v2_0_0/TransactionRequestsTest.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala index 0de719b86..40033ace3 100644 --- a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala @@ -29,7 +29,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // No challenge, No FX (same currencies) - if (Props.getBool("transactionRequests_enabled", false)) { + if (Props.getBool("transactionRequests_enabled", false) == false) { ignore("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) {} } else { scenario("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) { @@ -168,7 +168,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers } // No challenge, with FX - if (Props.getBool("transactionRequests_enabled", false)) { + if (Props.getBool("transactionRequests_enabled", false) == false) { ignore("we create an FX transaction request without challenge, with FX (different currencies)", TransactionRequest) {} } else { scenario("we create an FX transaction request without challenge, with FX (different currencies)", TransactionRequest) { @@ -380,7 +380,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // With challenge, No FX (Same currencies) - if (Props.getBool("transactionRequests_enabled", false)) { + if (Props.getBool("transactionRequests_enabled", false) == false) { ignore("we create a transaction request with a challenge, same currencies", TransactionRequest) {} } else { scenario("we create a transaction request with a challenge", TransactionRequest) { @@ -551,7 +551,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // With Challenge, with FX - if (Props.getBool("transactionRequests_enabled", false)) { + if (Props.getBool("transactionRequests_enabled", false) == false) { ignore("we create an FX transaction request with challenge", TransactionRequest) {} } else { scenario("we create an FX transaction request with challenge", TransactionRequest) { From 66ed043772ddaa8f68a00dcdab3b55e3749ffa14 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 25 May 2016 02:16:09 +0200 Subject: [PATCH 476/702] Adding getCurrentUser, CreateUser returns the User (WIP) --- .../scala/code/api/v2_0_0/APIMethods200.scala | 43 +++++++++++++- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 56 +++++++++++++++++++ .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- src/main/scala/code/model/User.scala | 3 + .../scala/code/model/dataAccess/OBPUser.scala | 7 +++ 5 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 23369a9af..09b8de75a 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1306,8 +1306,12 @@ trait APIMethods200 { .password(postedData.password) .validated(true) // TODO Get this from Props .save - if (userCreated) - successJsonResponse(JsRaw("{}"), 201) + if (userCreated) { + // Do we have to get again? + val obpUser = OBPUser.find(By(OBPUser.email, postedData.email)) + val json = JSONFactory200.createUserJSONfromOBPUser(obpUser) + successJsonResponse(Extraction.decompose(json), 201) + } else Full(errorJsonResponse("Error occurred during user creation.")) } @@ -1540,11 +1544,46 @@ trait APIMethods200 { + resourceDocs += ResourceDoc( + getCurrentUser, + apiVersion, + "getMeeting", + "GET", + "/users/current", + "Get Current User", + """Get the logged in user + | + |Login is required. + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagUser)) + + + lazy val getCurrentUser: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "users" :: "current" :: Nil JsonGet _ => { + user => + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + } + yield { + // Format the data as V2.0.0 json + val json = JSONFactory200.createUserJSON(u) + successJsonResponse(Extraction.decompose(json)) + } + } + } + /// + } } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 06f96798b..bc194a130 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -36,7 +36,9 @@ import java.util.Date import code.TransactionTypes.TransactionType.TransactionType import code.meetings.Meeting +import code.model.dataAccess.OBPUser import code.transactionrequests.TransactionRequests._ +import net.liftweb.common.{Full, Box} // import code.api.util.APIUtil.ApiLink @@ -460,6 +462,60 @@ object JSONFactory200{ + // + case class UserJSON( + id : String, + email : String, + provider_id: String, + provider : String, + display_name : String + ) + + + + + + def createUserJSONfromOBPUser(user : OBPUser) : UserJSON = { + new UserJSON( + id = "", // user.user.apiId.toString, // TODO we need to return the id. Future: It should be a uuid separate from any primary key. + email = user.email, + provider_id = stringOrNull(user.provider), + provider = stringOrNull(user.provider), + display_name = stringOrNull(user.displayName()) + ) + } + + + def createUserJSON(user : User) : UserJSON = { + new UserJSON( + id = user.apiId.toString, + email = user.emailAddress, + provider_id = user.idGivenByProvider, + provider = stringOrNull(user.provider), + display_name = stringOrNull(user.name) //TODO: Rename to displayName ? + ) + } + + def createUserJSON(user : Box[User]) : UserJSON = { + user match { + case Full(u) => createUserJSON(u) + case _ => null + } + } + + + + def createUserJSONfromOBPUser(user : Box[OBPUser]) : UserJSON = { + user match { + case Full(u) => createUserJSONfromOBPUser(u) + case _ => null + } + } + + + + + def createCoreTransactionDetailsJSON(transaction : ModeratedTransaction) : CoreTransactionDetailsJSON = { new CoreTransactionDetailsJSON( `type` = stringOptionOrNull(transaction.transactionType), diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 9aa4c9a1f..2cda1148e 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -168,7 +168,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.createMeeting, Implementations2_0_0.getMeetings, Implementations2_0_0.getMeeting, - Implementations2_0_0.createCustomer + Implementations2_0_0.createCustomer, + Implementations2_0_0.getCurrentUser ) routes.foreach(route => { diff --git a/src/main/scala/code/model/User.scala b/src/main/scala/code/model/User.scala index 777d62c2c..9b14c75c4 100644 --- a/src/main/scala/code/model/User.scala +++ b/src/main/scala/code/model/User.scala @@ -45,6 +45,9 @@ case class UserId(val value : Long) { override def toString = value.toString } + +// TODO Document clearly the difference between this and OBPUser + trait User { def apiId : UserId diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index f37fa3491..76006b9d4 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -45,6 +45,10 @@ import scala.xml.{NodeSeq, Text} /** * An O-R mapped "User" class that includes first name, last name, password + * + * + * // TODO Document the difference between this and User / APIUser + * */ class OBPUser extends MegaProtoUser[OBPUser] with Logger { def getSingleton = OBPUser // what's the "meta" server @@ -268,6 +272,9 @@ import net.liftweb.util.Helpers._ S.error("login", S.?("Invalid Username or Password")) } + + // What if we just want to return the userId without sending username/password?? + def getUserId(username: String, password: String): Long = { findUserByUserName(username) match { case Full(user) => { From fb3bf8a1970748de3ed4487cdf4e6d8322965ed6 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 25 May 2016 02:27:18 +0200 Subject: [PATCH 477/702] fixing getCurrentUser string in ResourceDoc --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 09b8de75a..a6ddc5a8e 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1547,7 +1547,7 @@ trait APIMethods200 { resourceDocs += ResourceDoc( getCurrentUser, apiVersion, - "getMeeting", + "getCurrentUser", // TODO can we get this string from the val two lines above? "GET", "/users/current", "Get Current User", From 22d251bd6153410767f2c978073ab31945239ac2 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 25 May 2016 13:14:12 +0200 Subject: [PATCH 478/702] Code cleanup --- .../bankconnectors/KafkaMappedConnector.scala | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 3e0e99378..cbb2338b0 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -132,7 +132,13 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def viewExists(acc: KafkaInboundAccount): Boolean = { - Views.views.vend.permittedViews(User.findByApiId(acc.id.toLong).orNull, getBankAccount(BankId(acc.bank), AccountId(acc.id)).orNull).nonEmpty + val res = ViewImpl.findAll.filter { v => + if (v.bankPermalink.get == acc.bank && v.accountPermalink.get == acc.id ) + true + else + false + } + res.nonEmpty } def createSaveableViews(acc : KafkaInboundAccount) : List[Saveable[ViewType]] = { @@ -879,12 +885,16 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable if (r.details.completed != null) // && r.details.completed.matches("^[0-9]{8}$")) dateCompleted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.completed) - val o = getBankAccountType(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).get + val c = getBankAccountType(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).orNull + val o = getBankAccountType(BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) - val dummyOtherBankAccount = createOtherBankAccount(o, None) + val dummyOtherBankAccount = createOtherBankAccount(c, o, None) //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init - val otherAccount = createOtherBankAccount(o, Some(dummyOtherBankAccount.metadata)) + val otherAccount = createOtherBankAccount(c, o, Some(dummyOtherBankAccount.metadata)) + + println("metadata: " + Counterparties.counterparties.vend.getOrCreateMetadata(BankId(r.this_account.bank), AccountId(r.this_account.id), otherAccount)) + // Create new transaction new Transaction( r.id, // uuid:String @@ -914,18 +924,18 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } // Helper for creating other bank account - def createOtherBankAccount(r: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { + def createOtherBankAccount(c: KafkaBankAccount, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { new OtherBankAccount( - id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), - label = r.label, - nationalIdentifier = r.nationalIdentifier, //TODO - swift_bic = Some(r.swift_bic.get), //TODO - iban = Some(r.iban.get), - number = r.number, - bankName = r.bankName, - kind = r.accountType, - originalPartyBankId = new BankId(r.bankId.value), - originalPartyAccountId = new AccountId(r.accountId.value), + id = c.accountId.value, //alreadyFoundMetadata.map(_.metadataId).getOrElse(""), + label = c.label, + nationalIdentifier = c.nationalIdentifier, //TODO + swift_bic = Some(c.swift_bic.get), //TODO + iban = Some(c.iban.get), + number = c.number, + bankName = c.bankName, + kind = c.accountType, + originalPartyBankId = BankId(o.bankId.value), + originalPartyAccountId = AccountId(o.accountId.value), alreadyFoundMetadata = alreadyFoundMetadata ) } From 2fcb8f8df10e2f9a20d197b6283dcf7212736f86 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 25 May 2016 14:54:39 +0200 Subject: [PATCH 479/702] Fixed missing counterparties when using kafka bankconnector --- .../bankconnectors/KafkaMappedConnector.scala | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index cbb2338b0..e7357930c 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -311,6 +311,34 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable Full(res) } + def getOtherBankAccount(thisAccountBankId : BankId, thisAccountId : AccountId, metadata : OtherBankAccountMetadata) : Box[OtherBankAccount] = { + //because we don't have a db backed model for OtherBankAccounts, we need to construct it from an + //OtherBankAccountMetadata and a transaction + val t = getTransactions(thisAccountBankId, thisAccountId).map { t => + t.filter { e => + if (e.otherAccount.number == metadata.getAccountNumber) + true + else + false + } + }.get.head + + val res = new OtherBankAccount( + //counterparty id is defined to be the id of its metadata as we don't actually have an id for the counterparty itself + id = metadata.metadataId, + label = metadata.getHolder, + nationalIdentifier = t.otherAccount.nationalIdentifier, + swift_bic = None, + iban = t.otherAccount.iban, + number = metadata.getAccountNumber, + bankName = t.otherAccount.bankName, + kind = t.otherAccount.kind, + originalPartyBankId = thisAccountBankId, + originalPartyAccountId = thisAccountId, + alreadyFoundMetadata = Some(metadata) + ) + Full(res) + } /** * @@ -351,33 +379,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable By(MappedAccountHolder.accountPermalink, accountID.value)).map(accHolder => accHolder.user.obj).flatten.toSet - def getOtherBankAccount(thisAccountBankId : BankId, thisAccountId : AccountId, metadata : OtherBankAccountMetadata) : Box[OtherBankAccount] = { - //because we don't have a db backed model for OtherBankAccounts, we need to construct it from an - //OtherBankAccountMetadata and a transaction - for { //find a transaction with this counterparty - t <- MappedTransaction.find( - By(MappedTransaction.bank, thisAccountBankId.value), - By(MappedTransaction.account, thisAccountId.value), - By(MappedTransaction.counterpartyAccountHolder, metadata.getHolder), - By(MappedTransaction.counterpartyAccountNumber, metadata.getAccountNumber)) - } yield { - new OtherBankAccount( - //counterparty id is defined to be the id of its metadata as we don't actually have an id for the counterparty itself - id = metadata.metadataId, - label = metadata.getHolder, - nationalIdentifier = t.counterpartyNationalId.get, - swift_bic = None, - iban = t.getCounterpartyIban(), - number = metadata.getAccountNumber, - bankName = t.counterpartyBankName.get, - kind = t.counterpartyAccountKind.get, - originalPartyBankId = thisAccountBankId, - originalPartyAccountId = thisAccountId, - alreadyFoundMetadata = Some(metadata) - ) - } - } - // Get all counterparties related to an account override def getOtherBankAccounts(bankId: BankId, accountID: AccountId): List[OtherBankAccount] = Counterparties.counterparties.vend.getMetadatas(bankId, accountID).flatMap(getOtherBankAccount(bankId, accountID, _)) @@ -893,8 +894,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init val otherAccount = createOtherBankAccount(c, o, Some(dummyOtherBankAccount.metadata)) - println("metadata: " + Counterparties.counterparties.vend.getOrCreateMetadata(BankId(r.this_account.bank), AccountId(r.this_account.id), otherAccount)) - // Create new transaction new Transaction( r.id, // uuid:String @@ -926,7 +925,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Helper for creating other bank account def createOtherBankAccount(c: KafkaBankAccount, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { new OtherBankAccount( - id = c.accountId.value, //alreadyFoundMetadata.map(_.metadataId).getOrElse(""), + id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), label = c.label, nationalIdentifier = c.nationalIdentifier, //TODO swift_bic = Some(c.swift_bic.get), //TODO From 747f0ca2a349e6b8b174e202ef3e92fc6156f928 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 26 May 2016 11:38:09 +0100 Subject: [PATCH 480/702] createCustomer -> 201 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index a6ddc5a8e..ad10642eb 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1537,7 +1537,7 @@ trait APIMethods200 { postedData.last_ok_date) ?~! "Could not create customer" } yield { val successJson = Extraction.decompose(customer) - successJsonResponse(successJson) + successJsonResponse(successJson, 201) } } } From 00a4b7682c603d0e9c29b6493f0044b85609850b Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 27 May 2016 15:28:17 +0200 Subject: [PATCH 481/702] This close ticket #25 --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 86f6a700c..0a4ebc8db 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -84,7 +84,7 @@ trait APIMethods121 { apiVersion, "root", "GET", - "", // Note this is empty i.e. we call /obp/v1.2.1 not /obp/v1.2.1/ + "/root", "The root of the API", """Returns information about: | @@ -100,7 +100,7 @@ trait APIMethods121 { apiTagApiInfo :: Nil) def root(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case Nil JsonGet json => { + case "root" :: Nil JsonGet json => { user => val apiDetails: JValue = { val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994") From 976d4753cd71ab7bd4a971ff01073947f35970ca Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 27 May 2016 16:13:34 +0200 Subject: [PATCH 482/702] Ticket #25 - fixed 2 failed tests --- src/test/scala/code/api/API121Test.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index 5643bc54c..5191f4e10 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -255,7 +255,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser } def getAPIInfo : APIResponse = { - val request = v1_2Request + val request = v1_2Request / "root" makeGetRequest(request) } From eb6a0776de0d8db631fc9b75d7d270ee093cfaca Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Sat, 28 May 2016 14:55:40 +0200 Subject: [PATCH 483/702] Ticket #25 - made paths /obp/vx.x.x and /obp/v.x.x.x/root as calls of root function --- .../scala/code/api/v1_2_1/APIMethods121.scala | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 0a4ebc8db..aa28ee20c 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -71,6 +71,15 @@ trait APIMethods121 { } yield metadata } + private def getApiInfoJSON(apiVersion : String) = { + val apiDetails: JValue = { + val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994") + val apiInfoJSON = new APIInfoJSON(apiVersion, gitCommit, hostedBy) + Extraction.decompose(apiInfoJSON) + } + apiDetails + } + // helper methods end here val Implementations1_2_1 = new Object(){ @@ -100,16 +109,8 @@ trait APIMethods121 { apiTagApiInfo :: Nil) def root(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "root" :: Nil JsonGet json => { - user => - val apiDetails: JValue = { - val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994") - val apiInfoJSON = new APIInfoJSON(apiVersion, gitCommit, hostedBy) - Extraction.decompose(apiInfoJSON) - } - - Full(successJsonResponse(apiDetails, 200)) - } + case "root" :: Nil JsonGet json => user => Full(successJsonResponse(getApiInfoJSON(apiVersion), 200)) + case Nil JsonGet json => user => Full(successJsonResponse(getApiInfoJSON(apiVersion), 200)) } From 894392b4ca374342e2fb4c728ac03a829e65762d Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 30 May 2016 18:53:48 +0200 Subject: [PATCH 484/702] #40 -- Hopefully commented all tests requiring git.properties --- src/test/scala/code/api/API121Test.scala | 4 ++-- src/test/scala/code/api/API12Test.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index 5191f4e10..f4a886a3b 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -1001,7 +1001,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser reply.code should equal (200) val apiInfo = reply.body.extract[APIInfoJSON] apiInfo.version should equal ("1.2.1") - apiInfo.git_commit.nonEmpty should equal (true) +/* apiInfo.git_commit.nonEmpty should equal (true)*/ } } @@ -6229,4 +6229,4 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser reply.body.extract[ErrorMessage].error.nonEmpty should equal (true) } } -} \ No newline at end of file +} diff --git a/src/test/scala/code/api/API12Test.scala b/src/test/scala/code/api/API12Test.scala index 1d498148b..650c0f57a 100644 --- a/src/test/scala/code/api/API12Test.scala +++ b/src/test/scala/code/api/API12Test.scala @@ -632,7 +632,7 @@ class API1_2Test extends User1AllPrivileges with DefaultUsers { reply.code should equal (200) val apiInfo = reply.body.extract[APIInfoJSON] apiInfo.version should equal ("1.2") - apiInfo.git_commit.nonEmpty should equal (true) +/* apiInfo.git_commit.nonEmpty should equal (true)*/ } } @@ -5589,4 +5589,4 @@ class API1_2Test extends User1AllPrivileges with DefaultUsers { reply.body.extract[ErrorMessage].error.nonEmpty should equal (true) } } -} \ No newline at end of file +} From 04de304a8c9c34d08f27b1d41e357e74344964ef Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 1 Jun 2016 11:08:30 +0200 Subject: [PATCH 485/702] Optimised createUser end point --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index ad10642eb..e3958957d 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1299,17 +1299,15 @@ trait APIMethods200 { postedData <- tryo {json.extract[CreateUserJSON]} ?~! ErrorMessages.InvalidJsonFormat } yield { if (OBPUser.find(By(OBPUser.email, postedData.email)).isEmpty) { - val userCreated:Boolean = OBPUser.create + val userCreated = OBPUser.create .firstName(postedData.first_name) .lastName(postedData.last_name) .email(postedData.email) .password(postedData.password) .validated(true) // TODO Get this from Props - .save - if (userCreated) { - // Do we have to get again? - val obpUser = OBPUser.find(By(OBPUser.email, postedData.email)) - val json = JSONFactory200.createUserJSONfromOBPUser(obpUser) + .saveMe() + if (userCreated.saved_?) { + val json = JSONFactory200.createUserJSONfromOBPUser(userCreated) successJsonResponse(Extraction.decompose(json), 201) } else From d693cd900a6de0dc4604f4cb004e3e0e69aa4f7c Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 1 Jun 2016 11:56:46 +0200 Subject: [PATCH 486/702] POST /user should return UUID for user_id #41 --- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 20 +++++++++---------- src/main/scala/code/model/BankingData.scala | 1 + src/main/scala/code/model/User.scala | 1 + .../scala/code/model/dataAccess/APIUser.scala | 4 ++++ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index bc194a130..f462dbfac 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -464,7 +464,7 @@ object JSONFactory200{ // case class UserJSON( - id : String, + user_id: String, email : String, provider_id: String, provider : String, @@ -475,20 +475,18 @@ object JSONFactory200{ - def createUserJSONfromOBPUser(user : OBPUser) : UserJSON = { - new UserJSON( - id = "", // user.user.apiId.toString, // TODO we need to return the id. Future: It should be a uuid separate from any primary key. - email = user.email, - provider_id = stringOrNull(user.provider), - provider = stringOrNull(user.provider), - display_name = stringOrNull(user.displayName()) - ) - } + def createUserJSONfromOBPUser(user : OBPUser) : UserJSON = new UserJSON( + user_id = user.user.foreign.get.userId, + email = user.email, + provider_id = stringOrNull(user.provider), + provider = stringOrNull(user.provider), + display_name = stringOrNull(user.displayName()) + ) def createUserJSON(user : User) : UserJSON = { new UserJSON( - id = user.apiId.toString, + user_id = user.userId, email = user.emailAddress, provider_id = user.idGivenByProvider, provider = stringOrNull(user.provider), diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 0616cdf94..825d7564f 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -282,6 +282,7 @@ trait BankAccount { //In this case, we just use the previous behaviour, which did not return very much information at all Set(new User { val apiId = UserId(-1) + val userId = "" val idGivenByProvider = "" val provider = "" val emailAddress = "" diff --git a/src/main/scala/code/model/User.scala b/src/main/scala/code/model/User.scala index 9b14c75c4..25309f3d0 100644 --- a/src/main/scala/code/model/User.scala +++ b/src/main/scala/code/model/User.scala @@ -51,6 +51,7 @@ case class UserId(val value : Long) { trait User { def apiId : UserId + def userId: String def idGivenByProvider: String def provider : String diff --git a/src/main/scala/code/model/dataAccess/APIUser.scala b/src/main/scala/code/model/dataAccess/APIUser.scala index b2f99f5d5..0470dcd2e 100644 --- a/src/main/scala/code/model/dataAccess/APIUser.scala +++ b/src/main/scala/code/model/dataAccess/APIUser.scala @@ -31,6 +31,7 @@ Berlin 13359, Germany */ package code.model.dataAccess +import code.util.MappedUUID import net.liftweb.mapper._ import net.liftweb.util.Props @@ -41,6 +42,7 @@ class APIUser extends LongKeyedMapper[APIUser] with User with ManyToMany with On def primaryKeyField = id object id extends MappedLongIndex(this) + object userId_ extends MappedUUID(this) object email extends MappedEmail(this, 48){ override def required_? = false } @@ -80,6 +82,8 @@ class APIUser extends LongKeyedMapper[APIUser] with User with ManyToMany with On def idGivenByProvider = providerId.get def apiId = UserId(id.get) + def userId = userId_.get + def name : String = name_.get def provider = provider_.get def views: List[View] = views_.toList From 25be438b0f072738b78e43b6f5211b149ae6fc6e Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 1 Jun 2016 15:53:18 +0200 Subject: [PATCH 487/702] Bumped copyright date in default template --- src/main/webapp/templates-hidden/default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index 061ceb0c6..a8af38053 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -115,7 +115,7 @@ Berlin 13359, Germany
    - Open Bank Project is ©2011-2015 TESOBE and distributed under the AGPL and commercial licenses.

    + Open Bank Project is ©2011-2016 TESOBE and distributed under the AGPL and commercial licenses.


    From 81d57c91bbbfb1e32956693e0628a5712316078d Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 3 Jun 2016 09:46:02 +0200 Subject: [PATCH 488/702] Add API call to Link User to Customer #35 - without script to migrate the data and modification of getCustomer in MappedCustomerProvider --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 40 ++++++++++- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 17 +++++ .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- src/main/scala/code/model/User.scala | 3 + .../MappedUserCustomerLinkProvider.scala | 66 +++++++++++++++++++ .../usercustomerlinks/UserCustomerLink.scala | 29 ++++++++ src/main/scala/code/users/LiftUsers.scala | 4 ++ src/main/scala/code/users/Users.scala | 2 + 9 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala create mode 100644 src/main/scala/code/usercustomerlinks/UserCustomerLink.scala diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index f030311d4..7cef09f5e 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -63,6 +63,7 @@ import code.transaction_types.MappedTransactionType import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} import code.tesobe.CashAccountAPI import code.transactionrequests.MappedTransactionRequest +import code.usercustomerlinks.MappedUserCustomerLink import net.liftweb.common._ import net.liftweb.http._ import net.liftweb.mapper._ @@ -417,5 +418,6 @@ object ToSchemify { MappedKycStatus, MappedSocialMedia, MappedTransactionType, - MappedMeeting) + MappedMeeting, + MappedUserCustomerLink) } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index e3958957d..76cf063de 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -29,6 +29,7 @@ import code.socialmedia.SocialMediaHandle import code.transactionrequests.TransactionRequests import code.meetings.Meeting +import code.usercustomerlinks.UserCustomerLink import net.liftweb.common.{Full, _} import net.liftweb.http.rest.RestHelper @@ -1515,8 +1516,7 @@ trait APIMethods200 { postedData <- tryo{json.extract[CreateCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists // TODO The user id we expose should be a uuid . For now we have a long direct from the database. - user_id <- tryo {postedData.user_id.toLong} ?~ ErrorMessages.InvalidNumber - customer_user <- User.findByApiId(user_id) ?~! ErrorMessages.UserNotFoundById + customer_user <- User.findByUserId(postedData.user_id) ?~! ErrorMessages.UserNotFoundById customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, customer_user).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser customer <- Customer.customerProvider.vend.addCustomer(bankId, customer_user, @@ -1576,7 +1576,43 @@ trait APIMethods200 { } } + resourceDocs += ResourceDoc( + createUserCustomerLinks, + apiVersion, + "createUserCustomerLinks", + "POST", + "/banks/BANK_ID/UserCustomerLinks", + "Create user customer link.", + """Link a customer and an user + |This call may require additional permissions/role in the future. + |For now the authenticated user can create at most one linked customer. + |OAuth authentication is required. + |""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + false, + false, + false, + List(apiTagCustomer)) + lazy val createUserCustomerLinks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "banks" :: BankId(bankId) :: "UserCustomerLinks" :: Nil JsonPost json -> _ => { + user => + for { + u <- user ?~! "User must be logged in to post Customer" + bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + user_id <- tryo(u.userId.isEmpty) ?~! "Field user_id is not defined for the logged user!" + customer_user <- User.findByUserId(u.userId) ?~! ErrorMessages.UserNotFoundById + customer <- tryo(Customer.customerProvider.vend.getCustomer(bankId, customer_user).get) ?~! ErrorMessages.CustomerNotFound + userCustomerLink <- booleanToBox(UserCustomerLink.userCustomerLinkProvider.vend.getUserCustomerLink(u.userId, customer.customerId).isEmpty == true) ?~ ErrorMessages.CustomerAlreadyExistsForUser + userCustomerLink <- UserCustomerLink.userCustomerLinkProvider.vend.createUserCustomerLink(u.userId, customer.customerId, bankId.value.toString, exampleDate, true) ?~! "Could not create userCustomerLink" + } yield { + val successJson = Extraction.decompose(code.api.v2_0_0.JSONFactory200.createUserCustomerLinkJSON(userCustomerLink)) + successJsonResponse(successJson, 201) + } + } + } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index f462dbfac..927df331b 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -111,6 +111,12 @@ case class MeetingPresentJSON( ) +case class UserCustomerLinkJSON(customer_id: String, + user_id: String, + bank_id: String, + date_inserted: Date, + is_active: Boolean) +case class UserCustomerLinkJSONs(l: List[UserCustomerLinkJSON]) class BasicViewJSON( val id: String, @@ -748,7 +754,18 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy MeetingJSONs(meetings.map(createMeetingJSON)) } + def createUserCustomerLinkJSON(ucl: code.usercustomerlinks.UserCustomerLink) = { + UserCustomerLinkJSON(customer_id = ucl.customerId, + user_id = ucl.userId, + bank_id = ucl.bankId, + date_inserted = ucl.dateInserted, + is_active = ucl.isActive + ) + } + def createUserCustomerLinkJSONs(ucls: List[code.usercustomerlinks.UserCustomerLink]): UserCustomerLinkJSONs = { + UserCustomerLinkJSONs(ucls.map(createUserCustomerLinkJSON)) + } // Copied from 1.2.1 (import just this def instead?) def stringOrNull(text : String) = diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 2cda1148e..100ad03fe 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -169,7 +169,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getMeetings, Implementations2_0_0.getMeeting, Implementations2_0_0.createCustomer, - Implementations2_0_0.getCurrentUser + Implementations2_0_0.getCurrentUser, + Implementations2_0_0.createUserCustomerLinks ) routes.foreach(route => { diff --git a/src/main/scala/code/model/User.scala b/src/main/scala/code/model/User.scala index 25309f3d0..b1dcdb4a9 100644 --- a/src/main/scala/code/model/User.scala +++ b/src/main/scala/code/model/User.scala @@ -100,4 +100,7 @@ object User { //versions of the API return this failure message, so if you change it, make sure //that all stable versions retain the same behavior Users.users.vend.getUserByProviderId(provider, idGivenByProvider) ~> UserNotFound(provider, idGivenByProvider) + + def findByUserId(userId : String) = + Users.users.vend.getUserByUserId(userId) } \ No newline at end of file diff --git a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala new file mode 100644 index 000000000..e65c1e9b0 --- /dev/null +++ b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala @@ -0,0 +1,66 @@ +package code.usercustomerlinks + +import java.util.Date + +import code.customer.Customer +import code.model.{User, BankId} +import code.util.{DefaultStringField} +import net.liftweb.common.Box +import net.liftweb.mapper._ + +/** + * Created by markom on 5/30/16. + */ +object MappedUserCustomerLinkProvider extends UserCustomerLinkProvider { + + override def createUserCustomerLink(userId: String, customerId: String, bankId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] = { + + val createUserCustomerLink = MappedUserCustomerLink.create + .mUserId(userId) + .mCustomerId(customerId) + .mBankId(bankId) + .mDateInserted(new Date()) + .mIsActive(isActive) + .saveMe() + + Some(createUserCustomerLink) + } + + override def getUserCustomerLink(userId : String, customerId: String): Box[UserCustomerLink] = { + MappedUserCustomerLink.find( + By(MappedUserCustomerLink.mUserId, userId), + By(MappedUserCustomerLink.mCustomerId, customerId)) + } + + override def getUserCustomerLinks: Box[List[UserCustomerLink]] = { + //MappedUserCustomerLink.bulkDelete_!!() + Some(MappedUserCustomerLink.findAll()) + } + +} + +class MappedUserCustomerLink extends UserCustomerLink with LongKeyedMapper[MappedUserCustomerLink] with IdPK with CreatedUpdated { + + def getSingleton = MappedUserCustomerLink + + // Name the objects m* so that we can give the overridden methods nice names. + // Assume we'll have to override all fields so name them all m* + object mCustomerId extends DefaultStringField(this) + object mBankId extends DefaultStringField(this) + object mUserId extends DefaultStringField(this) + object mDateInserted extends MappedDateTime(this) + object mIsActive extends MappedBoolean(this) + + override def customerId: String = mCustomerId.get // id.toString + override def userId: String = mUserId.get + override def bankId : String = mBankId.get + override def dateInserted: Date = mDateInserted.get + override def isActive: Boolean = mIsActive + + + +} + +object MappedUserCustomerLink extends MappedUserCustomerLink with LongKeyedMetaMapper[MappedUserCustomerLink] { + override def dbIndexes = UniqueIndex(mUserId, mCustomerId) :: super.dbIndexes +} diff --git a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala new file mode 100644 index 000000000..20975b09e --- /dev/null +++ b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala @@ -0,0 +1,29 @@ +package code.usercustomerlinks + +import java.util.Date +import code.model.{User, BankId} +import net.liftweb.common.Box +import net.liftweb.util.SimpleInjector + +trait UserCustomerLink { + def userId: String + def customerId: String + def bankId: String + def dateInserted: Date + def isActive: Boolean +} + + +object UserCustomerLink extends SimpleInjector { + + val userCustomerLinkProvider = new Inject(buildOne _) {} + + def buildOne: UserCustomerLinkProvider = MappedUserCustomerLinkProvider + +} + +trait UserCustomerLinkProvider { + def createUserCustomerLink(userId: String, customerId: String, bankId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] + def getUserCustomerLink(userId: String, customerId: String): Box[UserCustomerLink] + def getUserCustomerLinks: Box[List[UserCustomerLink]] +} \ No newline at end of file diff --git a/src/main/scala/code/users/LiftUsers.scala b/src/main/scala/code/users/LiftUsers.scala index 3bfa3b0a0..70dd773a0 100644 --- a/src/main/scala/code/users/LiftUsers.scala +++ b/src/main/scala/code/users/LiftUsers.scala @@ -15,5 +15,9 @@ private object LiftUsers extends Users { def getUserByProviderId(provider : String, idGivenByProvider : String) : Box[User] = { APIUser.find(By(APIUser.provider_, provider), By(APIUser.providerId, idGivenByProvider)) } + + def getUserByUserId(userId : String) : Box[User] = { + APIUser.find(By(APIUser.userId_, userId)) + } } \ No newline at end of file diff --git a/src/main/scala/code/users/Users.scala b/src/main/scala/code/users/Users.scala index ec099a4d4..a72951c00 100644 --- a/src/main/scala/code/users/Users.scala +++ b/src/main/scala/code/users/Users.scala @@ -16,4 +16,6 @@ trait Users { def getUserByApiId(id : Long) : Box[User] def getUserByProviderId(provider : String, idGivenByProvider : String) : Box[User] + + def getUserByUserId(userId : String) : Box[User] } \ No newline at end of file From 7457ce94d549f551209247374d87538aac521430 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 3 Jun 2016 15:10:22 +0100 Subject: [PATCH 489/702] Yearly Customer Charges WIP --- .../MappedYearlyCustomerChargeProvider.scala | 80 +++++++++++++++++++ .../YearlyCustomerChargeProvider.scala | 45 +++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala create mode 100644 src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala diff --git a/src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala b/src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala new file mode 100644 index 000000000..b1d9826eb --- /dev/null +++ b/src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala @@ -0,0 +1,80 @@ +package code.yearly_customer_charges + +import java.util.Date + +import code.customer.{CustomerFaceImage, Customer, CustomerProvider, MappedCustomer} +import code.model.{CustomerId, AmountOfMoney, BankId, User} +import code.model.dataAccess.{MappedBank, APIUser} +import code.util.{DefaultStringField, MappedUUID} +import net.liftweb.common.Box +import net.liftweb.mapper._ + + +// +//object MappedYearlyCustomerChargeProvider extends YearlyCustomerChargeProvider { +// +// +// +// override def getChargesForCustomer(bankId : String, customerId: String): List[YearlyCustomerCharge] = { +// MappedYearlyCustomerCharge.findAll( +// By(MappedYearlyCustomerCharge.mBankId, bankId), +// By(MappedYearlyCustomerCharge.mCustomerId, customerId) +// ) +// } +// +// +// +// override def addYearlyCharge(bankId: String, customerId: String) : Box[YearlyCustomerCharge] = { +// +// val createdCharge = MappedYearlyCustomerCharge.create +// .customerId("123".toInt).SaveMe +// +// Some(createdCharge) +// } +// +//} +// +// +// +// +//class MappedYearlyCustomerCharge extends YearlyCustomerCharge with LongKeyedMapper[MappedYearlyCustomerCharge] with IdPK with CreatedUpdated { +// +// def getSingleton = MappedYearlyCustomerCharge +// +// object mBankId extends DefaultStringField(this) +// object mCustomerId extends DefaultStringField(this) +// object mCustomerNumber extends DefaultStringField(this) +// +// object mYear extends MappedInt(this) +// +// object mCategoryId extends DefaultStringField(this) +// object mForcastIndictor extends DefaultStringField(this) +// object mTypeId extends DefaultStringField(this) +// object mNatureId extends DefaultStringField(this) +// +// +// object mCharge_Currency extends DefaultStringField(this) +// object mCharge_Amount extends DefaultStringField(this) +// +// object mUpdateDate extends MappedDateTime(this) +// +// +// override def bankId: String = mBankId.get +// override def customerId: String = mCustomerId.get // id.toString +// override def customerNumber: String = mCustomerNumber.get +// override def year: Integer = mYear.get +// +// override def categoryId: String = mCategoryId.get +// override def forcastIndictor: String = mForcastIndictor.get +// override def typeId: String = mTypeId.get +// override def natureId : String = mNatureId.get +// +// override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) +// override def updateDate : Date = mUpdateDate.get +// +// +//} +// +//object MappedYearlyCustomerCharge extends MappedYearlyCustomerCharge with LongKeyedMetaMapper[YearlyCustomerCharge] { +// override def dbIndexes = super.dbIndexes +//} \ No newline at end of file diff --git a/src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala b/src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala new file mode 100644 index 000000000..e5244603d --- /dev/null +++ b/src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala @@ -0,0 +1,45 @@ +package code.yearly_customer_charges + +import java.util.Date + +import code.customer.MappedCustomerProvider +import code.model.{CustomerId, AmountOfMoney, BankId, User} +import net.liftweb.common.Box +import net.liftweb.util.SimpleInjector + +// +//object YearlyCustomerCharge extends SimpleInjector { +// +// val yearlyCustomerChargeProvider = new Inject(buildOne _) {} +// +// def buildOne: YearlyCustomerChargeProvider = MappedYearlyCustomerChargeProvider +// +//} +// +//trait YearlyCustomerChargeProvider { +// +// def getChargesForCustomer(bankId : BankId, customerId: CustomerId) : List[YearlyCustomerCharge] +// +// +// def addChargeForCustomer(charge: YearlyCustomerCharge) +// +//} +// +// +//// This defines what a YearlyCustomerCharge looks like +//trait YearlyCustomerCharge { +// +// def bankId : String +// def customerId : String // The UUID for the customer. To be used in URLs +// def customerNumber : String +// +// def year : Integer +// +// def categoryId : String +// def forcastIndictor : String +// def typeId : String +// def natureId : String +// def charge : AmountOfMoney +// def updateDate : Date +// +//} From 7b2728e0cced07ff4073061aa742602b693ca62f Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 3 Jun 2016 17:01:55 +0200 Subject: [PATCH 490/702] Add bank level entitlements / roles #42 - without item number 7 --- .../resources/props/sample.props.template | 4 + src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- src/main/scala/code/api/util/APIUtil.scala | 10 +++ src/main/scala/code/api/util/ApiRole.scala | 18 +++++ .../scala/code/api/v2_0_0/APIMethods200.scala | 73 +++++++++++++++++++ .../code/api/v2_0_0/JSONFactory2.0.0.scala | 14 ++++ .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 4 +- .../scala/code/entitlement/Entilement.scala | 25 +++++++ .../MappedEntitlementsProvider.scala | 57 +++++++++++++++ 9 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/code/api/util/ApiRole.scala create mode 100644 src/main/scala/code/entitlement/Entilement.scala create mode 100644 src/main/scala/code/entitlement/MappedEntitlementsProvider.scala diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index ee3d1ee8e..31597fc8b 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -170,3 +170,7 @@ apiOptions.getTransactionTypesIsPublic = true # Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID # e.g. developers could use /my/accounts as well as /my/banks/BANK_ID/accounts defaultBank.bank_id=THE_DEFAULT_BANK_ID + + +# Super Admin Users (not database so we don't have to edit database) +super_admin_user_ids=USER_ID1,USER_ID2, \ No newline at end of file diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 7cef09f5e..7babd4194 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -42,6 +42,7 @@ import code.atms.MappedAtm import code.branches.MappedBranch import code.crm.MappedCrmEvent import code.customer.{MappedCustomer, MappedCustomerMessage} +import code.entitlement.MappedEntitlement import code.kycdocuments.MappedKycDocument import code.kycmedias.MappedKycMedia import code.kycchecks.MappedKycCheck @@ -419,5 +420,6 @@ object ToSchemify { MappedSocialMedia, MappedTransactionType, MappedMeeting, - MappedUserCustomerLink) + MappedUserCustomerLink, + MappedEntitlement) } diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 7671f080e..2f9566a03 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -34,6 +34,7 @@ package code.api.util import code.api.Constant._ import code.api.v1_2.ErrorMessage +import code.entitlement.Entitlements import code.metrics.APIMetrics import code.model._ import dispatch.url @@ -572,4 +573,13 @@ object APIUtil extends Loggable { halLinksJson } + def isSuperAdmin(user_id: String) : Boolean = { + val user_ids = Props.get("super_admin_user_ids", "super_admin_user_ids is not defined").split(",").map(_.trim).toList + user_ids.filter(_ == user_id).length > 0 + } + + def hasEntitlement(bankId: String, userId: String, role: ApiRole): Boolean = { + Entitlements.entitlementProvider.vend.getEntitlement(bankId, userId, role.toString).isEmpty + } + } diff --git a/src/main/scala/code/api/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala new file mode 100644 index 000000000..2f8c93a27 --- /dev/null +++ b/src/main/scala/code/api/util/ApiRole.scala @@ -0,0 +1,18 @@ +package code.api.util + +sealed trait ApiRole + +object ApiRole { + + case object CanSearchAllTransactions extends ApiRole + case object CanSearchAllAccounts extends ApiRole + case object CanQueryOtherUser extends ApiRole + + def valueOf(value: String): ApiRole = value match { + case "CanSearchAllTransactions" => CanSearchAllTransactions + case "CanSearchAllAccounts" => CanSearchAllAccounts + case "CanQueryOtherUser" => CanQueryOtherUser + case _ => throw new IllegalArgumentException() + } + +} \ No newline at end of file diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 76cf063de..b88a4f347 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -6,6 +6,7 @@ import java.util.Calendar import code.TransactionTypes.TransactionType import code.api.APIFailure import code.api.util.APIUtil._ +import code.api.util.ApiRole._ import code.api.util.ErrorMessages import code.api.v1_2_1.OBPAPI1_2_1._ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121, JSONFactory => JSONFactory121} @@ -30,6 +31,7 @@ import code.transactionrequests.TransactionRequests import code.meetings.Meeting import code.usercustomerlinks.UserCustomerLink +import code.entitlement.Entitlements import net.liftweb.common.{Full, _} import net.liftweb.http.rest.RestHelper @@ -1614,8 +1616,79 @@ trait APIMethods200 { } } + resourceDocs += ResourceDoc( + addEntitlements, + apiVersion, + "addEntitlements", + "POST", + "/users/USER_ID/entitlements", + "Add role to specific user.", + """Grants the Role to user. + | + |OAuth authentication is required and the user needs to have access to the owner view.""", + Extraction.decompose(CreateEntitlementJSON("obp-bank-x-gh", "CanQueryOtherUser")), + emptyObjectJson, + emptyObjectJson :: Nil, + false, + false, + false, + List()) + + lazy val addEntitlements : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + //add access for specific user to a list of views + case "users" :: userId :: "entitlements" :: Nil JsonPost json -> _ => { + user => + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + isSuperAdmin <- booleanToBox(isSuperAdmin(u.userId)) ?~ "User is not super admin!" + postedData <- tryo{json.extract[CreateEntitlementJSON]} ?~ "wrong format JSON" + role <- tryo{valueOf(postedData.role_name)} ?~! "wrong role name" + hasEntitlement <- booleanToBox(hasEntitlement(postedData.bank_id, u.userId, role)) ?~ "Entitlement already exists" + addedEntitlement <- Entitlements.entitlementProvider.vend.addEntitlement(postedData.bank_id, u.userId, postedData.role_name) + } yield { + val viewJson = JSONFactory200.createEntitlementJSON(addedEntitlement) + successJsonResponse(Extraction.decompose(viewJson), 201) + } + } + } + + resourceDocs += ResourceDoc( + getEntitlements, + apiVersion, + "getEntitlements", + "GET", + "/users/USER_ID/entitlements", + "Get Entitlement specified by USER_ID", + """ + | + |Login is required. + | + | + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) + lazy val getEntitlements: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "users" :: userId :: "entitlements" :: Nil JsonGet _ => { + user => + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + isSuperAdmin <- booleanToBox(isSuperAdmin(u.userId)) ?~ "User is not super admin!" + entitlements <- Entitlements.entitlementProvider.vend.getEntitlements(u.userId) + } + yield { + // Format the data as V2.0.0 json + val json = JSONFactory200.createEntitlementJSONs(entitlements) + successJsonResponse(Extraction.decompose(json)) + } + } + } /// } diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 927df331b..960b4d16a 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -36,6 +36,7 @@ import java.util.Date import code.TransactionTypes.TransactionType.TransactionType import code.meetings.Meeting +import code.entitlement.Entitlement import code.model.dataAccess.OBPUser import code.transactionrequests.TransactionRequests._ import net.liftweb.common.{Full, Box} @@ -310,6 +311,9 @@ case class TransactionRequestBodyJSON ( description : String ) +case class CreateEntitlementJSON(bank_id: String, role_name: String) +case class EntitlementJSON(entitlement_id: String, user_id: String, role_name: String) +case class EntitlementJSONs(list: List[EntitlementJSON]) object JSONFactory200{ @@ -767,6 +771,16 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy UserCustomerLinkJSONs(ucls.map(createUserCustomerLinkJSON)) } + def createEntitlementJSON(e: Entitlement): EntitlementJSON = { + EntitlementJSON(entitlement_id = e.entitlementId, + user_id = e.userId, + role_name = e.roleName) + } + + def createEntitlementJSONs(l: List[Entitlement]) = { + EntitlementJSONs(l.map(createEntitlementJSON)) + } + // Copied from 1.2.1 (import just this def instead?) def stringOrNull(text : String) = if(text == null || text.isEmpty) diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 100ad03fe..1e4d59203 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -170,7 +170,9 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getMeeting, Implementations2_0_0.createCustomer, Implementations2_0_0.getCurrentUser, - Implementations2_0_0.createUserCustomerLinks + Implementations2_0_0.createUserCustomerLinks, + Implementations2_0_0.addEntitlements, + Implementations2_0_0.getEntitlements ) routes.foreach(route => { diff --git a/src/main/scala/code/entitlement/Entilement.scala b/src/main/scala/code/entitlement/Entilement.scala new file mode 100644 index 000000000..f0e93516a --- /dev/null +++ b/src/main/scala/code/entitlement/Entilement.scala @@ -0,0 +1,25 @@ +package code.entitlement + + +import code.model.{BankId, User} +import net.liftweb.common.Box +import net.liftweb.util.SimpleInjector + + +object Entitlements extends SimpleInjector { + val entitlementProvider = new Inject(buildOne _) {} + def buildOne: EntitlementProvider = MappedEntitlementsProvider +} + +trait EntitlementProvider { + def getEntitlement(bankId: String, userId: String, roleName: String) : Box[Entitlement] + def getEntitlements(userId: String) : Box[List[Entitlement]] + def addEntitlement(bankId: String, userId: String, roleName: String) : Box[MappedEntitlement] +} + +trait Entitlement { + def entitlementId: String + def bankId : String + def userId : String + def roleName : String +} \ No newline at end of file diff --git a/src/main/scala/code/entitlement/MappedEntitlementsProvider.scala b/src/main/scala/code/entitlement/MappedEntitlementsProvider.scala new file mode 100644 index 000000000..7602bea49 --- /dev/null +++ b/src/main/scala/code/entitlement/MappedEntitlementsProvider.scala @@ -0,0 +1,57 @@ +package code.entitlement + + +import code.util.{MappedUUID, DefaultStringField} +import net.liftweb.mapper._ +import net.liftweb.common.Box + +object MappedEntitlementsProvider extends EntitlementProvider { + + override def getEntitlement(bankId: String, userId: String, roleName: String): Box[MappedEntitlement] = { + // Return a Box so we can handle errors later. + MappedEntitlement.find( + By(MappedEntitlement.mBankId, bankId), + By(MappedEntitlement.mUserId, userId), + By(MappedEntitlement.mRoleName, roleName) + ) + } + + override def getEntitlements(userId: String): Box[List[MappedEntitlement]] = { + // Return a Box so we can handle errors later. + Some(MappedEntitlement.findAll( + By(MappedEntitlement.mUserId, userId), + OrderBy(MappedEntitlement.updatedAt, Descending))) + } + + + override def addEntitlement(bankId: String, userId: String, roleName: String): Box[MappedEntitlement] = { + // Return a Box so we can handle errors later. + val addEntitlement = MappedEntitlement.create + .mBankId(bankId) + .mUserId(userId) + .mRoleName(roleName) + .saveMe() + Some(addEntitlement) + } +} + +class MappedEntitlement extends Entitlement +with LongKeyedMapper[MappedEntitlement] with IdPK with CreatedUpdated { + + def getSingleton = MappedEntitlement + + object mEntitlementId extends MappedUUID(this) + object mBankId extends DefaultStringField(this) + object mUserId extends DefaultStringField(this) + object mRoleName extends DefaultStringField(this) + + override def entitlementId: String = mEntitlementId.get.toString + override def bankId: String = mBankId.get + override def userId: String = mUserId.get + override def roleName: String = mRoleName.get + +} + +object MappedEntitlement extends MappedEntitlement with LongKeyedMetaMapper[MappedEntitlement] { + override def dbIndexes = UniqueIndex(mEntitlementId) :: UniqueIndex(mUserId, mBankId) :: super.dbIndexes +} \ No newline at end of file From b1957ecfba973643afc76241cd1c118eed05aad1 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 4 Jun 2016 15:42:40 +0100 Subject: [PATCH 491/702] Added Example Thing WIP --- .../examplething/MappedThing1Provider.scala | 117 ++++++++++++++++++ src/main/scala/code/examplething/Thing1.scala | 88 +++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 src/main/scala/code/examplething/MappedThing1Provider.scala create mode 100644 src/main/scala/code/examplething/Thing1.scala diff --git a/src/main/scala/code/examplething/MappedThing1Provider.scala b/src/main/scala/code/examplething/MappedThing1Provider.scala new file mode 100644 index 000000000..642aaf638 --- /dev/null +++ b/src/main/scala/code/examplething/MappedThing1Provider.scala @@ -0,0 +1,117 @@ +package code.examplething + +import code.branches.Branches._ +import code.model.BankId + +import code.common.{Address, License, Location, Meta} + +import code.util.DefaultStringField +import net.liftweb.common.Box +import net.liftweb.mapper._ +import org.joda.time.Hours + +import scala.util.Try + +object MappedThing1Provider extends Thing1Provider { + + override protected def getBranchFromProvider(branchId: BranchId): Option[Branch] = + MappedThing1$.find(By(MappedThing1$.mBranchId, branchId.value)) + + override protected def getBranchesFromProvider(bankId: BankId): Option[List[Branch]] = { + Some(MappedThing1$.findAll(By(MappedThing1$.mBankId, bankId.value))) + } +} + +class MappedThing1$ extends Branch with LongKeyedMapper[MappedThing1$] with IdPK { + + override def getSingleton = MappedThing1$ + + object mBankId extends DefaultStringField(this) + object mName extends DefaultStringField(this) + + object mBranchId extends DefaultStringField(this) + + // Exposed inside address. See below + object mLine1 extends DefaultStringField(this) + object mLine2 extends DefaultStringField(this) + object mLine3 extends DefaultStringField(this) + object mCity extends DefaultStringField(this) + object mCounty extends DefaultStringField(this) + object mState extends DefaultStringField(this) + object mCountryCode extends MappedString(this, 2) + object mPostCode extends DefaultStringField(this) + + object mlocationLatitude extends MappedDouble(this) + object mlocationLongitude extends MappedDouble(this) + + // Exposed inside meta.license See below + object mLicenseId extends DefaultStringField(this) + object mLicenseName extends DefaultStringField(this) + + object mLobbyHours extends DefaultStringField(this) + object mDriveUpHours extends DefaultStringField(this) + + override def branchId: BranchId = BranchId(mBranchId.get) + override def name: String = mName.get + + override def address: Address = new Address { + override def line1: String = mLine1.get + override def line2: String = mLine2.get + override def line3: String = mLine3.get + override def city: String = mCity.get + override def county: String = mCounty.get + override def state: String = mState.get + override def countryCode: String = mCountryCode.get + override def postCode: String = mPostCode.get + } + + override def meta: Meta = new Meta { + override def license: License = new License { + override def id: String = mLicenseId.get + override def name: String = mLicenseName.get + } + } + + override def lobby: Lobby = new Lobby { + override def hours: String = mLobbyHours + } + + override def driveUp: DriveUp = new DriveUp { + override def hours: String = mDriveUpHours + } + + + override def location: Location = new Location { + override def latitude: Double = mlocationLatitude + override def longitude: Double = mlocationLongitude + } + +} + +// +object MappedThing1$ extends MappedThing1$ with LongKeyedMetaMapper[MappedThing1$] { + override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes +} + +/* +For storing the data license(s) (conceived for open data e.g. branches) +Currently used as one license per bank for all open data? +Else could store a link to this with each open data record - or via config for each open data type + */ + + +//class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdPK { +// override def getSingleton = MappedLicense +// +// object mBankId extends DefaultStringField(this) +// object mName extends DefaultStringField(this) +// object mUrl extends DefaultStringField(this) +// +// override def name: String = mName.get +// override def url: String = mUrl.get +//} +// +// +//object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { +// override def dbIndexes = Index(mBankId) :: super.dbIndexes +//} \ No newline at end of file diff --git a/src/main/scala/code/examplething/Thing1.scala b/src/main/scala/code/examplething/Thing1.scala new file mode 100644 index 000000000..c8db616e5 --- /dev/null +++ b/src/main/scala/code/examplething/Thing1.scala @@ -0,0 +1,88 @@ +package code.examplething + + +// Need to import these one by one because in same package! +import code.branches.Branches.{Branch, BranchId} + +import code.common.{Address, License, Location, Meta} + +import code.model.{BankId} +import net.liftweb.common.Logger +import net.liftweb.util.SimpleInjector + +object Thing1 extends SimpleInjector { + + case class Thing1Id(value : String) + + trait Thing1 { + def branchId : Thing1Id + def name : String + def address : Address + def location : Location + def thing2 : Thing2 + def thing3 : Thing3 + def meta : Meta + } + + trait Thing2 { + def hours : String + } + + trait Thing3 { + def hours : String + } + + val thing1Provider = new Inject(buildOne _) {} + + def buildOne: Thing1Provider = MappedThing1Provider + + + // Helper to get the count out of an option + def countOfThing1(listOpt: Option[List[Thing1]]) : Int = { + val count = listOpt match { + case Some(list) => list.size + case None => 0 + } + count + } + + +} + +trait Thing1Provider { + + private val logger = Logger(classOf[Thing1Provider]) + + + /* + Common logic for returning branches. + Implementation details in branchesData + */ + final def getThing1s(bankId : BankId) : Option[List[Branch]] = { + // If we get branches filter them + getBranchesFromProvider(bankId) match { + case Some(branches) => { + + val branchesWithLicense = for { + branch <- branches if branch.meta.license.name.size > 3 + } yield branch + Option(branchesWithLicense) + } + case None => None + } + } + + /* + Return one Branch + */ + final def getThing(branchId : BranchId) : Option[Branch] = { + // Filter out if no license data + getBranchFromProvider(branchId).filter(x => x.meta.license.id != "" && x.meta.license.name != "") + } + + protected def getBranchFromProvider(branchId : BranchId) : Option[Branch] + protected def getBranchesFromProvider(bank : BankId) : Option[List[Branch]] + +// End of Trait +} + From 66d6fdae873468e716f4e6e3fcdfe5948ab94396 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 4 Jun 2016 16:05:33 +0100 Subject: [PATCH 492/702] Example Thing WIP 2 --- ...ovider.scala => MappedThingProvider.scala} | 45 +++++-------------- .../{Thing1.scala => Thing.scala} | 44 +++++++++--------- 2 files changed, 34 insertions(+), 55 deletions(-) rename src/main/scala/code/examplething/{MappedThing1Provider.scala => MappedThingProvider.scala} (60%) rename src/main/scala/code/examplething/{Thing1.scala => Thing.scala} (50%) diff --git a/src/main/scala/code/examplething/MappedThing1Provider.scala b/src/main/scala/code/examplething/MappedThingProvider.scala similarity index 60% rename from src/main/scala/code/examplething/MappedThing1Provider.scala rename to src/main/scala/code/examplething/MappedThingProvider.scala index 642aaf638..cc93b927b 100644 --- a/src/main/scala/code/examplething/MappedThing1Provider.scala +++ b/src/main/scala/code/examplething/MappedThingProvider.scala @@ -1,6 +1,7 @@ package code.examplething import code.branches.Branches._ +import code.examplething.Thing.{Bar, Foo, ThingId, Thing} import code.model.BankId import code.common.{Address, License, Location, Meta} @@ -12,19 +13,19 @@ import org.joda.time.Hours import scala.util.Try -object MappedThing1Provider extends Thing1Provider { +object MappedThingProvider extends ThingProvider { - override protected def getBranchFromProvider(branchId: BranchId): Option[Branch] = - MappedThing1$.find(By(MappedThing1$.mBranchId, branchId.value)) + override protected def getThingFromProvider(branchId: BranchId): Option[Thing] = + MappedThing.find(By(MappedThing.mBranchId, branchId.value)) - override protected def getBranchesFromProvider(bankId: BankId): Option[List[Branch]] = { - Some(MappedThing1$.findAll(By(MappedThing1$.mBankId, bankId.value))) + override protected def getThingsFromProvider(bankId: BankId): Option[List[Thing]] = { + Some(MappedThing.findAll(By(MappedThing.mBankId, bankId.value))) } } -class MappedThing1$ extends Branch with LongKeyedMapper[MappedThing1$] with IdPK { +class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK { - override def getSingleton = MappedThing1$ + override def getSingleton = MappedThing object mBankId extends DefaultStringField(this) object mName extends DefaultStringField(this) @@ -51,7 +52,7 @@ class MappedThing1$ extends Branch with LongKeyedMapper[MappedThing1$] with IdPK object mLobbyHours extends DefaultStringField(this) object mDriveUpHours extends DefaultStringField(this) - override def branchId: BranchId = BranchId(mBranchId.get) + override def thingId: ThingId = ThingId(mBranchId.get) override def name: String = mName.get override def address: Address = new Address { @@ -72,11 +73,11 @@ class MappedThing1$ extends Branch with LongKeyedMapper[MappedThing1$] with IdPK } } - override def lobby: Lobby = new Lobby { + override def foo: Foo = new Foo { override def hours: String = mLobbyHours } - override def driveUp: DriveUp = new DriveUp { + override def bar: Bar = new Bar { override def hours: String = mDriveUpHours } @@ -89,29 +90,7 @@ class MappedThing1$ extends Branch with LongKeyedMapper[MappedThing1$] with IdPK } // -object MappedThing1$ extends MappedThing1$ with LongKeyedMetaMapper[MappedThing1$] { +object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] { override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes } -/* -For storing the data license(s) (conceived for open data e.g. branches) -Currently used as one license per bank for all open data? -Else could store a link to this with each open data record - or via config for each open data type - */ - - -//class MappedLicense extends License with LongKeyedMapper[MappedLicense] with IdPK { -// override def getSingleton = MappedLicense -// -// object mBankId extends DefaultStringField(this) -// object mName extends DefaultStringField(this) -// object mUrl extends DefaultStringField(this) -// -// override def name: String = mName.get -// override def url: String = mUrl.get -//} -// -// -//object MappedLicense extends MappedLicense with LongKeyedMetaMapper[MappedLicense] { -// override def dbIndexes = Index(mBankId) :: super.dbIndexes -//} \ No newline at end of file diff --git a/src/main/scala/code/examplething/Thing1.scala b/src/main/scala/code/examplething/Thing.scala similarity index 50% rename from src/main/scala/code/examplething/Thing1.scala rename to src/main/scala/code/examplething/Thing.scala index c8db616e5..cd5d74fd7 100644 --- a/src/main/scala/code/examplething/Thing1.scala +++ b/src/main/scala/code/examplething/Thing.scala @@ -5,40 +5,41 @@ package code.examplething import code.branches.Branches.{Branch, BranchId} import code.common.{Address, License, Location, Meta} +import code.examplething.Thing.Thing import code.model.{BankId} import net.liftweb.common.Logger import net.liftweb.util.SimpleInjector -object Thing1 extends SimpleInjector { +object Thing extends SimpleInjector { - case class Thing1Id(value : String) + case class ThingId(value : String) - trait Thing1 { - def branchId : Thing1Id + trait Thing { + def thingId : ThingId def name : String def address : Address def location : Location - def thing2 : Thing2 - def thing3 : Thing3 + def foo : Foo + def bar : Bar def meta : Meta } - trait Thing2 { + trait Foo { def hours : String } - trait Thing3 { + trait Bar { def hours : String } val thing1Provider = new Inject(buildOne _) {} - def buildOne: Thing1Provider = MappedThing1Provider + def buildOne: ThingProvider = MappedThingProvider // Helper to get the count out of an option - def countOfThing1(listOpt: Option[List[Thing1]]) : Int = { + def countOfThing1(listOpt: Option[List[Thing]]) : Int = { val count = listOpt match { case Some(list) => list.size case None => 0 @@ -49,18 +50,17 @@ object Thing1 extends SimpleInjector { } -trait Thing1Provider { +trait ThingProvider { - private val logger = Logger(classOf[Thing1Provider]) + private val logger = Logger(classOf[ThingProvider]) /* - Common logic for returning branches. - Implementation details in branchesData + Common logic for returning Things + Implementation details in Thing provider */ - final def getThing1s(bankId : BankId) : Option[List[Branch]] = { - // If we get branches filter them - getBranchesFromProvider(bankId) match { + final def getThings(bankId : BankId) : Option[List[Thing]] = { + getThingsFromProvider(bankId) match { case Some(branches) => { val branchesWithLicense = for { @@ -73,15 +73,15 @@ trait Thing1Provider { } /* - Return one Branch + Return one Thing */ - final def getThing(branchId : BranchId) : Option[Branch] = { + final def getThing(branchId : BranchId) : Option[Thing] = { // Filter out if no license data - getBranchFromProvider(branchId).filter(x => x.meta.license.id != "" && x.meta.license.name != "") + getThingFromProvider(branchId).filter(x => x.meta.license.id != "" && x.meta.license.name != "") } - protected def getBranchFromProvider(branchId : BranchId) : Option[Branch] - protected def getBranchesFromProvider(bank : BankId) : Option[List[Branch]] + protected def getThingFromProvider(branchId : BranchId) : Option[Thing] + protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] // End of Trait } From 9a12f8b5367f2a6ad057060849366803a5ac3d37 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 5 Jun 2016 00:43:11 +0200 Subject: [PATCH 493/702] Thing 1, Thing 2 example --- .../examplething/MappedThingProvider.scala | 74 ++++------------- src/main/scala/code/examplething/Thing.scala | 79 ++++++++----------- 2 files changed, 51 insertions(+), 102 deletions(-) diff --git a/src/main/scala/code/examplething/MappedThingProvider.scala b/src/main/scala/code/examplething/MappedThingProvider.scala index cc93b927b..8fdf0991d 100644 --- a/src/main/scala/code/examplething/MappedThingProvider.scala +++ b/src/main/scala/code/examplething/MappedThingProvider.scala @@ -1,25 +1,22 @@ package code.examplething -import code.branches.Branches._ -import code.examplething.Thing.{Bar, Foo, ThingId, Thing} + import code.model.BankId -import code.common.{Address, License, Location, Meta} import code.util.DefaultStringField import net.liftweb.common.Box import net.liftweb.mapper._ -import org.joda.time.Hours -import scala.util.Try + object MappedThingProvider extends ThingProvider { - override protected def getThingFromProvider(branchId: BranchId): Option[Thing] = - MappedThing.find(By(MappedThing.mBranchId, branchId.value)) + override protected def getThingFromProvider(thingId: ThingId): Option[Thing] = + MappedThing.find(By(MappedThing.thingId_, thingId.value)) override protected def getThingsFromProvider(bankId: BankId): Option[List[Thing]] = { - Some(MappedThing.findAll(By(MappedThing.mBankId, bankId.value))) + Some(MappedThing.findAll(By(MappedThing.bankId_, bankId.value))) } } @@ -27,70 +24,31 @@ class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK { override def getSingleton = MappedThing - object mBankId extends DefaultStringField(this) - object mName extends DefaultStringField(this) + object bankId_ extends DefaultStringField(this) + object name_ extends DefaultStringField(this) - object mBranchId extends DefaultStringField(this) + object thingId_ extends DefaultStringField(this) - // Exposed inside address. See below - object mLine1 extends DefaultStringField(this) - object mLine2 extends DefaultStringField(this) - object mLine3 extends DefaultStringField(this) - object mCity extends DefaultStringField(this) - object mCounty extends DefaultStringField(this) - object mState extends DefaultStringField(this) - object mCountryCode extends MappedString(this, 2) - object mPostCode extends DefaultStringField(this) + object fooSomething_ extends DefaultStringField(this) + object barSomething_ extends DefaultStringField(this) - object mlocationLatitude extends MappedDouble(this) - object mlocationLongitude extends MappedDouble(this) + override def thingId: ThingId = ThingId(thingId_.get) + override def something: String = name_.get - // Exposed inside meta.license See below - object mLicenseId extends DefaultStringField(this) - object mLicenseName extends DefaultStringField(this) - - object mLobbyHours extends DefaultStringField(this) - object mDriveUpHours extends DefaultStringField(this) - - override def thingId: ThingId = ThingId(mBranchId.get) - override def name: String = mName.get - - override def address: Address = new Address { - override def line1: String = mLine1.get - override def line2: String = mLine2.get - override def line3: String = mLine3.get - override def city: String = mCity.get - override def county: String = mCounty.get - override def state: String = mState.get - override def countryCode: String = mCountryCode.get - override def postCode: String = mPostCode.get - } - - override def meta: Meta = new Meta { - override def license: License = new License { - override def id: String = mLicenseId.get - override def name: String = mLicenseName.get - } - } override def foo: Foo = new Foo { - override def hours: String = mLobbyHours + override def fooSomething: String = fooSomething_ } override def bar: Bar = new Bar { - override def hours: String = mDriveUpHours + override def barSomething: String = barSomething_ } - override def location: Location = new Location { - override def latitude: Double = mlocationLatitude - override def longitude: Double = mlocationLongitude - } - } -// + object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] { - override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes + override def dbIndexes = UniqueIndex(bankId_, thingId_) :: Index(bankId_) :: super.dbIndexes } diff --git a/src/main/scala/code/examplething/Thing.scala b/src/main/scala/code/examplething/Thing.scala index cd5d74fd7..49946be02 100644 --- a/src/main/scala/code/examplething/Thing.scala +++ b/src/main/scala/code/examplething/Thing.scala @@ -5,7 +5,7 @@ package code.examplething import code.branches.Branches.{Branch, BranchId} import code.common.{Address, License, Location, Meta} -import code.examplething.Thing.Thing + import code.model.{BankId} import net.liftweb.common.Logger @@ -13,59 +13,51 @@ import net.liftweb.util.SimpleInjector object Thing extends SimpleInjector { - case class ThingId(value : String) - - trait Thing { - def thingId : ThingId - def name : String - def address : Address - def location : Location - def foo : Foo - def bar : Bar - def meta : Meta - } - - trait Foo { - def hours : String - } - - trait Bar { - def hours : String - } - - val thing1Provider = new Inject(buildOne _) {} - - def buildOne: ThingProvider = MappedThingProvider - - - // Helper to get the count out of an option - def countOfThing1(listOpt: Option[List[Thing]]) : Int = { - val count = listOpt match { - case Some(list) => list.size - case None => 0 - } - count - } + val thingProvider = new Inject(buildOne _) {} + def buildOne: ThingProvider = MappedThingProvider } +case class ThingId(value : String) + +trait Thing { + def thingId : ThingId + def something : String + def foo : Foo + def bar : Bar +} + +trait Foo { + def fooSomething : String +} + +trait Bar { + def barSomething : String +} + + +/* +A trait that defines interfaces to Thing +i.e. a ThingProvider should provide these: + */ + trait ThingProvider { private val logger = Logger(classOf[ThingProvider]) /* - Common logic for returning Things - Implementation details in Thing provider + Common logic for returning or changing Things + Datasource implementation details in Thing provider */ final def getThings(bankId : BankId) : Option[List[Thing]] = { getThingsFromProvider(bankId) match { - case Some(branches) => { + case Some(things) => { val branchesWithLicense = for { - branch <- branches if branch.meta.license.name.size > 3 - } yield branch + thing <- things // if thing.meta.license.name.size > 3 + } yield thing Option(branchesWithLicense) } case None => None @@ -75,14 +67,13 @@ trait ThingProvider { /* Return one Thing */ - final def getThing(branchId : BranchId) : Option[Thing] = { - // Filter out if no license data - getThingFromProvider(branchId).filter(x => x.meta.license.id != "" && x.meta.license.name != "") + final def getThing(thingId : ThingId) : Option[Thing] = { + // Could do something here + getThingFromProvider(thingId) //.filter... } - protected def getThingFromProvider(branchId : BranchId) : Option[Thing] + protected def getThingFromProvider(branchId : ThingId) : Option[Thing] protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] -// End of Trait } From a624bebe5dc0dd9ae28f75136912c466b0dc6da4 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 5 Jun 2016 01:15:54 +0200 Subject: [PATCH 494/702] Thing provider from Props --- src/main/scala/code/examplething/Thing.scala | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/scala/code/examplething/Thing.scala b/src/main/scala/code/examplething/Thing.scala index 49946be02..e53999ebb 100644 --- a/src/main/scala/code/examplething/Thing.scala +++ b/src/main/scala/code/examplething/Thing.scala @@ -2,21 +2,26 @@ package code.examplething // Need to import these one by one because in same package! -import code.branches.Branches.{Branch, BranchId} - -import code.common.{Address, License, Location, Meta} +import code.bankconnectors.{KafkaMappedConnector, LocalConnector, LocalMappedConnector} import code.model.{BankId} import net.liftweb.common.Logger -import net.liftweb.util.SimpleInjector +import net.liftweb.util.{Props, SimpleInjector} object Thing extends SimpleInjector { val thingProvider = new Inject(buildOne _) {} - def buildOne: ThingProvider = MappedThingProvider + // def buildOne: ThingProvider = MappedThingProvider + // This determines the provider we use + def buildOne: ThingProvider = + Props.get("provider.thing").openOr("mapped") match { + case "mapped" => MappedThingProvider + case _ => MappedThingProvider + } + } case class ThingId(value : String) @@ -49,16 +54,16 @@ trait ThingProvider { /* Common logic for returning or changing Things - Datasource implementation details in Thing provider + Datasource implementation details are in Thing provider */ final def getThings(bankId : BankId) : Option[List[Thing]] = { getThingsFromProvider(bankId) match { case Some(things) => { - val branchesWithLicense = for { + val certainThings = for { thing <- things // if thing.meta.license.name.size > 3 } yield thing - Option(branchesWithLicense) + Option(certainThings) } case None => None } @@ -72,7 +77,7 @@ trait ThingProvider { getThingFromProvider(thingId) //.filter... } - protected def getThingFromProvider(branchId : ThingId) : Option[Thing] + protected def getThingFromProvider(thingId : ThingId) : Option[Thing] protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] } From 64ef59a52d508c71b7d41c74dec4f325ec4ef954 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 6 Jun 2016 11:45:24 +0200 Subject: [PATCH 495/702] Create Role canSearchWarehouse - https://trello.com/c/BX1ANKG5/9-create-role-cansearchwarehouse --- src/main/scala/code/api/util/ApiRole.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/scala/code/api/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala index 2f8c93a27..929a1e46b 100644 --- a/src/main/scala/code/api/util/ApiRole.scala +++ b/src/main/scala/code/api/util/ApiRole.scala @@ -7,11 +7,13 @@ object ApiRole { case object CanSearchAllTransactions extends ApiRole case object CanSearchAllAccounts extends ApiRole case object CanQueryOtherUser extends ApiRole + case object CanSearchWarehouse extends ApiRole def valueOf(value: String): ApiRole = value match { case "CanSearchAllTransactions" => CanSearchAllTransactions case "CanSearchAllAccounts" => CanSearchAllAccounts case "CanQueryOtherUser" => CanQueryOtherUser + case "CanSearchWarehouse" => CanSearchWarehouse case _ => throw new IllegalArgumentException() } From 26a4a0fe59641662dccfaeb9a8d441925d27f760 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 6 Jun 2016 16:26:43 +0200 Subject: [PATCH 496/702] Add API call to Link User to Customer #35 - cleaned code --- .../code/usercustomerlinks/MappedUserCustomerLinkProvider.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala index e65c1e9b0..d35efa30b 100644 --- a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala +++ b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala @@ -33,7 +33,6 @@ object MappedUserCustomerLinkProvider extends UserCustomerLinkProvider { } override def getUserCustomerLinks: Box[List[UserCustomerLink]] = { - //MappedUserCustomerLink.bulkDelete_!!() Some(MappedUserCustomerLink.findAll()) } From 978be5cd74e2c48f29a63b687faafdb683b57aaf Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 6 Jun 2016 17:43:21 +0200 Subject: [PATCH 497/702] Added overrride CSS for unicredit --- src/main/webapp/media/css/overrides/unicredit.css | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/main/webapp/media/css/overrides/unicredit.css diff --git a/src/main/webapp/media/css/overrides/unicredit.css b/src/main/webapp/media/css/overrides/unicredit.css new file mode 100644 index 000000000..d9db3e99d --- /dev/null +++ b/src/main/webapp/media/css/overrides/unicredit.css @@ -0,0 +1,4 @@ +#header-decoration, #authorizeSection { + background-color: #d60006; + height: 4px; +} From 1f01ef65d7c7f39cd2974f61fd0e3da7154aed63 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 6 Jun 2016 21:29:18 +0200 Subject: [PATCH 498/702] Added preliminary version of elasticsearch (warehouse and metrics). Currently only portions of code working --- pom.xml | 10 ++ .../resources/props/sample.props.template | 11 ++ src/main/scala/bootstrap/liftweb/Boot.scala | 5 + src/main/scala/code/api/OBPRestHelper.scala | 8 +- src/main/scala/code/api/elasticsearch.scala | 33 +++++ src/main/scala/code/api/util/APIUtil.scala | 22 +++- .../scala/code/api/v2_0_0/APIMethods200.scala | 45 ++++++- src/main/scala/code/metrics/APIMetrics.scala | 11 +- .../scala/code/metrics/MappedMetrics.scala | 12 +- .../scala/code/metrics/MongoAPIMetric.scala | 27 ++-- src/main/scala/code/search/search.scala | 122 ++++++++++++++++++ 11 files changed, 281 insertions(+), 25 deletions(-) create mode 100644 src/main/scala/code/api/elasticsearch.scala create mode 100644 src/main/scala/code/search/search.scala diff --git a/pom.xml b/pom.xml index fcf927db5..f0074d340 100644 --- a/pom.xml +++ b/pom.xml @@ -181,6 +181,16 @@ opentok-server-sdk 3.0.0-beta.1 + + org.elasticsearch + elasticsearch + 2.3.0 + + + com.sksamuel.elastic4s + elastic4s-core_2.11 + 2.3.0 + diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template index 31597fc8b..3162142f2 100644 --- a/src/main/resources/props/sample.props.template +++ b/src/main/resources/props/sample.props.template @@ -22,6 +22,17 @@ connector=mapped #kafka.request_topic=Request #kafka.response_topic=Response +#ElasticSearch +allow_elasticsearch=true + +#elasticsearch warehouse +es_warehouse.host=http://localhost:9200 +es_warehouse.data_index=es_warehouse_data_index + +#elasticsearch metrics +es_metrics.host=http://localhost:9200 +es_metrics.data_index=es_metrics_data_index + #you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url #db.driver=org.postgresql.Driver #db.driver=org.h2.Driver diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 7babd4194..630e27d26 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -196,6 +196,11 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(DirectLogin) } + // ElasticSearch metrics endpoint + if(Props.getBool("allow_metrics_elasticsearch", true)) { + LiftRules.statelessDispatch.append(ElasticSearchMetrics) + } + // Add the various API versions LiftRules.statelessDispatch.append(v1_0.OBPAPI1_0) LiftRules.statelessDispatch.append(v1_1.OBPAPI1_1) diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala index f1ee71f43..93327887d 100644 --- a/src/main/scala/code/api/OBPRestHelper.scala +++ b/src/main/scala/code/api/OBPRestHelper.scala @@ -139,11 +139,9 @@ trait OBPRestHelper extends RestHelper with Loggable { case _ => errorJsonResponse("oauth error") } } else if (Props.getBool("allow_direct_login", true) && isThereDirectLoginHeader) { - val user = DirectLogin.getUser - if (user.isDefined) { - fn(user) - } else { - fn(Empty) + DirectLogin.getUser match { + case Full(u) => fn(Full(u)) + case _ => errorJsonResponse("directlogin error") } } else { fn(Empty) diff --git a/src/main/scala/code/api/elasticsearch.scala b/src/main/scala/code/api/elasticsearch.scala new file mode 100644 index 000000000..8ef4ae25b --- /dev/null +++ b/src/main/scala/code/api/elasticsearch.scala @@ -0,0 +1,33 @@ +package code.api + +import code.search._ +import net.liftweb.common.{Full, Loggable} +import net.liftweb.http.rest.RestHelper +import net.liftweb.http.{GetRequest, InMemoryResponse, Req} + +/** + * Created by petar on 6/1/16. + */ + +object ElasticSearchMetrics extends RestHelper with Loggable { + + val es = new elasticsearchMetrics + + serve { + case Req("search" :: Nil, json , GetRequest) => { + /*user => + for { + u <- user //?~ ErrorMessages.UserNotLoggedIn + hasEntitlement <- booleanToBox(Entitlements.entitlementProvider.vend.getEntitlement(u.bankId, u.userId, ApiRole.CanSearchAllAccounts)) ?~ "Entitlement already exists" + } yield { + Full(InMemoryResponse(es.searchProxy().getBytes(), Nil, Nil, 200)) + } + else { + //Full(errorJsonResponse(ErrorMessages.UserNotLoggedIn)) + */ + Full(InMemoryResponse(es.searchProxy(json).toString.getBytes(), Nil, Nil, 200)) + } + + } + } + diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 2f9566a03..cd0864fea 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -33,19 +33,21 @@ package code.api.util import code.api.Constant._ +import code.api.DirectLogin +import code.api.OAuthHandshake._ import code.api.v1_2.ErrorMessage import code.entitlement.Entitlements import code.metrics.APIMetrics import code.model._ import dispatch.url -import net.liftweb.common.{Box, Full, Loggable} +import net.liftweb.common.{Empty, _} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.js.JsExp import net.liftweb.http.{CurrentReq, JsonResponse, Req, S} import net.liftweb.json.JsonAST.JValue import net.liftweb.json.{Extraction, parse} import net.liftweb.util.Helpers._ -import net.liftweb.util.{Props, Helpers, SecurityHelpers} +import net.liftweb.util.{Helpers, Props, SecurityHelpers} import scala.collection.mutable.ArrayBuffer import scala.collection.JavaConverters._ @@ -134,8 +136,22 @@ object APIUtil extends Loggable { def logAPICall = { if(Props.getBool("write_metrics", false)) { + val user = + if (isThereAnOAuthHeader) { + getUser match { + case Full(u) => Full(u) + case _ => Empty + } + } else if (Props.getBool("allow_direct_login", true) && isThereDirectLoginHeader) { + DirectLogin.getUser match { + case Full(u) => Full(u) + case _ => Empty + } + } else { + Empty + } // TODO This should use Elastic Search or Kafka not an RDBMS - APIMetrics.apiMetrics.vend.saveMetric(S.uriAndQueryString.getOrElse(""), (now: TimeSpan)) + APIMetrics.apiMetrics.vend.saveMetric(user.orNull.userId, S.uriAndQueryString.getOrElse(""), (now: TimeSpan)) } } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index b88a4f347..bb843f2ef 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -7,10 +7,11 @@ import code.TransactionTypes.TransactionType import code.api.APIFailure import code.api.util.APIUtil._ import code.api.util.ApiRole._ -import code.api.util.ErrorMessages +import code.api.util.{ApiRole, ErrorMessages} import code.api.v1_2_1.OBPAPI1_2_1._ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121, JSONFactory => JSONFactory121} -import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, ChallengeAnswerJSON, TransactionRequestAccountJSON} +import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, CustomerFaceImageJson, TransactionRequestAccountJSON} +import code.search.{elasticsearch, elasticsearchWarehouse} //import code.api.v2_0_0.{CreateCustomerJson} import code.model.dataAccess.OBPUser @@ -1689,7 +1690,45 @@ trait APIMethods200 { } } } - /// + + resourceDocs += ResourceDoc( + elasticSearchWarehouse, + apiVersion, + "elasticSearchWarehouse", + "GET", + "/search", + "Elastic Search Warehouse", + """ + | + |Login is required. + | + |CanSearchWarehouse entitlement is requred for logged-in user. + | + | + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List()) + + val es = elasticsearchWarehouse + + lazy val elasticSearchWarehouse: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "search" :: Nil JsonGet json => { + user => + for { + u <- user ?~ ErrorMessages.UserNotLoggedIn + entitlements <- Entitlements.entitlementProvider.vend.getEntitlements(u.userId) + hasEntitlement <- booleanToBox(entitlements.contains(ApiRole.CanSearchWarehouse)) ?~ "User is not allowed to search warehouse!" + } yield { + successJsonResponse(Extraction.decompose(es.searchProxy(json.toString))) + } + } + } + } } diff --git a/src/main/scala/code/metrics/APIMetrics.scala b/src/main/scala/code/metrics/APIMetrics.scala index 5ed4e712d..823cfcbc2 100644 --- a/src/main/scala/code/metrics/APIMetrics.scala +++ b/src/main/scala/code/metrics/APIMetrics.scala @@ -29,7 +29,12 @@ object APIMetrics extends SimpleInjector { trait APIMetrics { - def saveMetric(url : String, date : Date) : Unit + def saveMetric(userId: String, url : String, date : Date) : Unit + + def saveMetric(url : String, date : Date) : Unit ={ + //TODO: update all places calling old function before removing this + saveMetric ("TODO: userId", url, date) + } //TODO: ordering of list? should this be by date? currently not enforced def getAllGroupedByUrl() : Map[String, List[APIMetric]] @@ -37,10 +42,14 @@ trait APIMetrics { //TODO: ordering of list? should this be alphabetically by url? currently not enforced def getAllGroupedByDay() : Map[Date, List[APIMetric]] + //TODO: ordering of list? should this be alphabetically by url? currently not enforced + def getAllGroupedByUserId() : Map[String, List[APIMetric]] + } trait APIMetric { def getUrl() : String def getDate() : Date + def getUserId() : String } diff --git a/src/main/scala/code/metrics/MappedMetrics.scala b/src/main/scala/code/metrics/MappedMetrics.scala index e4ea85537..0353a4710 100644 --- a/src/main/scala/code/metrics/MappedMetrics.scala +++ b/src/main/scala/code/metrics/MappedMetrics.scala @@ -7,10 +7,15 @@ import net.liftweb.mapper._ object MappedMetrics extends APIMetrics { - override def saveMetric(url: String, date: Date): Unit = { + override def saveMetric(userId: String, url: String, date: Date): Unit = { MappedMetric.create.url(url).date(date).save } + override def getAllGroupedByUserId(): Map[String, List[APIMetric]] = { + //TODO: do this all at the db level using an actual group by query + MappedMetric.findAll.groupBy(_.getUserId) + } + override def getAllGroupedByDay(): Map[Date, List[APIMetric]] = { //TODO: do this all at the db level using an actual group by query MappedMetric.findAll.groupBy(APIMetrics.getMetricDay) @@ -26,15 +31,16 @@ object MappedMetrics extends APIMetrics { class MappedMetric extends APIMetric with LongKeyedMapper[MappedMetric] with IdPK { override def getSingleton = MappedMetric + object userId extends DefaultStringField(this) object url extends DefaultStringField(this) object date extends MappedDateTime(this) override def getUrl(): String = url.get override def getDate(): Date = date.get - + override def getUserId(): String = userId.get } object MappedMetric extends MappedMetric with LongKeyedMetaMapper[MappedMetric] { - override def dbIndexes = Index(url) :: Index(date) :: super.dbIndexes + override def dbIndexes = Index(userId) :: Index(url) :: Index(date) :: super.dbIndexes } diff --git a/src/main/scala/code/metrics/MongoAPIMetric.scala b/src/main/scala/code/metrics/MongoAPIMetric.scala index ae2f327dd..d415f50f4 100644 --- a/src/main/scala/code/metrics/MongoAPIMetric.scala +++ b/src/main/scala/code/metrics/MongoAPIMetric.scala @@ -32,24 +32,28 @@ Berlin 13359, Germany package code.metrics -import net.liftweb.mongodb.record.field.{ObjectIdPk,DateField} - import net.liftweb.record.field.StringField - import net.liftweb.mongodb.record.{MongoRecord,MongoMetaRecord} - import java.util.{Calendar, Date} +import java.util.Date + +import net.liftweb.mongodb.record.field.{DateField, ObjectIdPk} +import net.liftweb.mongodb.record.{MongoMetaRecord, MongoRecord} +import net.liftweb.record.field.StringField private class MongoAPIMetric extends MongoRecord[MongoAPIMetric] with ObjectIdPk[MongoAPIMetric] with APIMetric { - def meta = MongoAPIMetric - object url extends StringField(this,255) - object date extends DateField(this) + def meta = MongoAPIMetric + object userId extends StringField(this,255) + object url extends StringField(this,255) + object date extends DateField(this) - def getUrl() = url.get - def getDate() = date.get + def getUrl() = url.get + def getDate() = date.get + def getUserId() = userId.get } private object MongoAPIMetric extends MongoAPIMetric with MongoMetaRecord[MongoAPIMetric] with APIMetrics { - def saveMetric(url : String, date : Date) : Unit = { + def saveMetric(userId: String, url : String, date : Date) : Unit = { MongoAPIMetric.createRecord. + userId(userId). url(url). date(date). save @@ -63,4 +67,7 @@ private object MongoAPIMetric extends MongoAPIMetric with MongoMetaRecord[MongoA MongoAPIMetric.findAll.groupBy[Date](APIMetrics.getMetricDay) } + def getAllGroupedByUserId() : Map[String, List[APIMetric]] = { + MongoAPIMetric.findAll.groupBy[String](_.getUserId) + } } diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala new file mode 100644 index 000000000..45dcf6695 --- /dev/null +++ b/src/main/scala/code/search/search.scala @@ -0,0 +1,122 @@ +package code.search + +import code.api.v1_2_1.{AccountJSON, TransactionJSON} +import com.sksamuel.elastic4s.ElasticClient +import com.sksamuel.elastic4s.ElasticDsl._ +import dispatch.{Http, url} +import net.liftweb.http.S +import net.liftweb.common.Box +import net.liftweb.util.Props +import net.liftweb.util.HttpHelpers +import org.elasticsearch.common.settings.Settings + +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import net.liftweb.common.{Box, Full, Loggable} +import net.liftweb.http.js.JsExp +import net.liftweb.http.{JsonResponse, LiftResponse, NoContentResponse, S} +import net.liftweb.http.rest._ +import net.liftweb.json.Extraction +import net.liftweb.json.JsonAST._ +import net.liftweb.json.Serialization.write +import net.liftweb.util.Helpers +import net.liftweb.util.Props +import dispatch._ +import Defaults._ +import net.liftweb.json + + +class elasticsearch { + + case class QueryStrings(esType: String, queryStr: String) + case class APIResponse(code: Int, body: JValue) + case class ErrorMessage(error: String) + + val esHost = "" + val esIndex = "" + + def getAPIResponse(req: Req): APIResponse = { + Await.result( + for (response <- Http(req > as.Response(p => p))) + yield { + val body = if (response.getResponseBody().isEmpty) "{}" else response.getResponseBody() + APIResponse(response.getStatusCode, json.parse(body)) + } + , Duration.Inf) + } + + def searchProxy(query: String): LiftResponse = { + val request = constructQuery(getParametersFromUrl) + val response = getAPIResponse(request) + JsonResponse(response.body, ("Access-Control-Allow-Origin","*") :: Nil, Nil, response.code) + } + + private def constructQuery(queries: QueryStrings): Req = { + val jsonQuery = s"""{"query": ${queries.queryStr}}""" + val esUrl = Helpers.appendParams(s"$esHost/$esIndex/${queries.esType}/_search", Seq(("source",jsonQuery))) + url(esUrl).GET + } + + def getParametersFromUrl: QueryStrings = { + val query: Box[Map[String, List[String]]] = S.request.map(_.params) + val esType = S.param("dataset").getOrElse("") + val queryStr = S.param("query").getOrElse("") + QueryStrings(esType, queryStr) + } + +} + + + + + +class elasticsearchLocal extends elasticsearch { + + override val esHost = Props.get("es_warehouse.host","http://localhost:9200") + override val esIndex = Props.get("es_warehouse.data_index","es_warehouse_data_index") + + val settings = Settings.settingsBuilder() + .put("http.enabled", true) + .put("path.home", "/tmp/elastic/") + + val client = ElasticClient.local(settings.build) + + /* + Index objects in Elastic Search. + Use **the same** representations that we return in the REST API. + Use the name singular_object_name-version e.g. transaction-v1.2.1 for the index name / type + */ + + // Index a Transaction + // Put into a index that has the viewId and version in the name. + def indexTransaction(viewId: String, transaction: TransactionJSON) { + client.execute { + index into "transaction_v1.2.1" fields { + viewId -> transaction + } + } + } + + def indexAccount(viewId: String, account: AccountJSON) { + client.execute { + index into "account_v1.2.1" fields { + viewId -> account + } + } + } + + } + + + object elasticsearchWarehouse extends elasticsearch { + override val esHost = Props.get("es_warehouse.host","http://localhost:9200") + override val esIndex = Props.get("es_warehouse.data_index","es_warehouse_data_index") + } + + + class elasticsearchMetrics extends elasticsearch { + override val esHost = Props.get("es_metrics.host","http://localhost:9200") + override val esIndex = Props.get("es_metrics.data_index","es_metrics_data_index") + } + + From 55b215a1dbd11a853eb31e20953d7f0530c32d59 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Jun 2016 14:02:16 +0200 Subject: [PATCH 499/702] Yearly Customer Charge WIP --- .../MappedYearlyCustomerChargeProvider.scala | 80 ------------------- .../YearlyCustomerChargeProvider.scala | 45 ----------- .../MappedYearlyChargeProvider.scala | 78 ++++++++++++++++++ .../yearlycustomercharges/YearlyCharge.scala | 73 +++++++++++++++++ 4 files changed, 151 insertions(+), 125 deletions(-) delete mode 100644 src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala delete mode 100644 src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala create mode 100644 src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala create mode 100644 src/main/scala/code/yearlycustomercharges/YearlyCharge.scala diff --git a/src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala b/src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala deleted file mode 100644 index b1d9826eb..000000000 --- a/src/main/scala/code/yearly_customer_charges/MappedYearlyCustomerChargeProvider.scala +++ /dev/null @@ -1,80 +0,0 @@ -package code.yearly_customer_charges - -import java.util.Date - -import code.customer.{CustomerFaceImage, Customer, CustomerProvider, MappedCustomer} -import code.model.{CustomerId, AmountOfMoney, BankId, User} -import code.model.dataAccess.{MappedBank, APIUser} -import code.util.{DefaultStringField, MappedUUID} -import net.liftweb.common.Box -import net.liftweb.mapper._ - - -// -//object MappedYearlyCustomerChargeProvider extends YearlyCustomerChargeProvider { -// -// -// -// override def getChargesForCustomer(bankId : String, customerId: String): List[YearlyCustomerCharge] = { -// MappedYearlyCustomerCharge.findAll( -// By(MappedYearlyCustomerCharge.mBankId, bankId), -// By(MappedYearlyCustomerCharge.mCustomerId, customerId) -// ) -// } -// -// -// -// override def addYearlyCharge(bankId: String, customerId: String) : Box[YearlyCustomerCharge] = { -// -// val createdCharge = MappedYearlyCustomerCharge.create -// .customerId("123".toInt).SaveMe -// -// Some(createdCharge) -// } -// -//} -// -// -// -// -//class MappedYearlyCustomerCharge extends YearlyCustomerCharge with LongKeyedMapper[MappedYearlyCustomerCharge] with IdPK with CreatedUpdated { -// -// def getSingleton = MappedYearlyCustomerCharge -// -// object mBankId extends DefaultStringField(this) -// object mCustomerId extends DefaultStringField(this) -// object mCustomerNumber extends DefaultStringField(this) -// -// object mYear extends MappedInt(this) -// -// object mCategoryId extends DefaultStringField(this) -// object mForcastIndictor extends DefaultStringField(this) -// object mTypeId extends DefaultStringField(this) -// object mNatureId extends DefaultStringField(this) -// -// -// object mCharge_Currency extends DefaultStringField(this) -// object mCharge_Amount extends DefaultStringField(this) -// -// object mUpdateDate extends MappedDateTime(this) -// -// -// override def bankId: String = mBankId.get -// override def customerId: String = mCustomerId.get // id.toString -// override def customerNumber: String = mCustomerNumber.get -// override def year: Integer = mYear.get -// -// override def categoryId: String = mCategoryId.get -// override def forcastIndictor: String = mForcastIndictor.get -// override def typeId: String = mTypeId.get -// override def natureId : String = mNatureId.get -// -// override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) -// override def updateDate : Date = mUpdateDate.get -// -// -//} -// -//object MappedYearlyCustomerCharge extends MappedYearlyCustomerCharge with LongKeyedMetaMapper[YearlyCustomerCharge] { -// override def dbIndexes = super.dbIndexes -//} \ No newline at end of file diff --git a/src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala b/src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala deleted file mode 100644 index e5244603d..000000000 --- a/src/main/scala/code/yearly_customer_charges/YearlyCustomerChargeProvider.scala +++ /dev/null @@ -1,45 +0,0 @@ -package code.yearly_customer_charges - -import java.util.Date - -import code.customer.MappedCustomerProvider -import code.model.{CustomerId, AmountOfMoney, BankId, User} -import net.liftweb.common.Box -import net.liftweb.util.SimpleInjector - -// -//object YearlyCustomerCharge extends SimpleInjector { -// -// val yearlyCustomerChargeProvider = new Inject(buildOne _) {} -// -// def buildOne: YearlyCustomerChargeProvider = MappedYearlyCustomerChargeProvider -// -//} -// -//trait YearlyCustomerChargeProvider { -// -// def getChargesForCustomer(bankId : BankId, customerId: CustomerId) : List[YearlyCustomerCharge] -// -// -// def addChargeForCustomer(charge: YearlyCustomerCharge) -// -//} -// -// -//// This defines what a YearlyCustomerCharge looks like -//trait YearlyCustomerCharge { -// -// def bankId : String -// def customerId : String // The UUID for the customer. To be used in URLs -// def customerNumber : String -// -// def year : Integer -// -// def categoryId : String -// def forcastIndictor : String -// def typeId : String -// def natureId : String -// def charge : AmountOfMoney -// def updateDate : Date -// -//} diff --git a/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala b/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala new file mode 100644 index 000000000..123fd8d72 --- /dev/null +++ b/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala @@ -0,0 +1,78 @@ +package code.yearlycustomercharges + +import code.model.{CustomerId, BankId} + + +import code.util.DefaultStringField +import net.liftweb.common.Box +import net.liftweb.mapper._ + + + +object MappedYearlyChargeProvider extends YearlyChargeProvider { + +// override protected def getYearlyChargeFromProvider(thingId: YearlyChargeId): Option[YearlyCharge] = +// MappedYearlyCharge.find(By(MappedYearlyCharge.thingId_, thingId.value)) + + override protected def getYearlyChargesFromProvider(bankId: BankId, customerId: CustomerId, year: Int): Option[List[YearlyCharge]] = { + Some(MappedYearlyCharge.findAll(By(MappedYearlyCharge.bankId_, bankId.value), By(MappedYearlyCharge.customerId_, customerId.value))) + } +} + +class MappedYearlyCharge extends YearlyCharge with LongKeyedMapper[MappedYearlyCharge] with IdPK { + + override def getSingleton = MappedYearlyCharge + + object bankId_ extends DefaultStringField(this) + object customerId_ extends DefaultStringField(this) + + object year_ extends MappedInt(this) + + + + //override def yearlyChargeId: YearlyChargeId = YearlyChargeId(id.get) + override def year: Int = year_.get + + + // override def getSingleton = MappedYearlyCustomerCharge + // + // WIP + // object mCustomerNumber extends DefaultStringField(this) + // + // object mYear extends MappedInt(this) + // + // object mCategoryId extends DefaultStringField(this) + // object mForcastIndictor extends DefaultStringField(this) + // object mTypeId extends DefaultStringField(this) + // object mNatureId extends DefaultStringField(this) + // + // + // object mCharge_Currency extends DefaultStringField(this) + // object mCharge_Amount extends DefaultStringField(this) + // + // object mUpdateDate extends MappedDateTime(this) + // + // + // //override def bankId: String = mBankId.get + // override def customerId: String = mCustomerId.get // id.toString + // override def customerNumber: String = mCustomerNumber.get + // override def year: Integer = mYear.get + // + // override def categoryId: String = mCategoryId.get + // override def forcastIndictor: String = mForcastIndictor.get + // override def typeId: String = mTypeId.get + // override def natureId : String = mNatureId.get + // + // override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) + // override def updateDate : Date = mUpdateDate.get + + + + +} + + +object MappedYearlyCharge extends MappedYearlyCharge with LongKeyedMetaMapper[MappedYearlyCharge] { + override def dbIndexes = UniqueIndex(bankId_, customerId_, year_) :: Index(bankId_) :: super.dbIndexes +} + diff --git a/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala b/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala new file mode 100644 index 000000000..572dede33 --- /dev/null +++ b/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala @@ -0,0 +1,73 @@ +package code.yearlycustomercharges + +import code.bankconnectors.{KafkaMappedConnector, LocalConnector, LocalMappedConnector} + + +import code.model.{CustomerId, BankId} +import net.liftweb.common.Logger +import net.liftweb.util.{Props, SimpleInjector} + +object YearlyCharge extends SimpleInjector { + + val yearlyChargeProvider = new Inject(buildOne _) {} + + + // This determines the provider we use + def buildOne: YearlyChargeProvider = + Props.get("provider.thing").openOr("mapped") match { + case "mapped" => MappedYearlyChargeProvider + case _ => MappedYearlyChargeProvider + } + +} + +case class YearlyChargeId(value : String) + +// WIP +trait YearlyCharge { + def year : Int + + // def customerNumber : String + // + // + // def categoryId : String + // def forcastIndictor : String + // def typeId : String + // def natureId : String + // def charge : AmountOfMoney + // def updateDate : Date + + + +} + + + + +trait YearlyChargeProvider { + + private val logger = Logger(classOf[YearlyChargeProvider]) + + + /* + Common logic for returning or changing Things + Datasource implementation details are in Thing provider + */ + final def getYearlyCharges(bankId : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] = { + getYearlyChargesFromProvider(bankId, customerId, year) match { + case Some(things) => { + + val certainThings = for { + thing <- things // if filter etc. if need be + } yield thing + Option(certainThings) + } + case None => None + } + } + + + protected def getYearlyChargesFromProvider(bank : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] + +} + From b3cd6b9dd09c4a288122d16a01948f98ca035c71 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Jun 2016 15:27:59 +0200 Subject: [PATCH 500/702] Adding search to 2.0.0 APIs --- .../scala/code/api/v2_0_0/APIMethods200.scala | 17 +++++++++-------- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index bb843f2ef..33960d872 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1624,9 +1624,9 @@ trait APIMethods200 { "POST", "/users/USER_ID/entitlements", "Add role to specific user.", - """Grants the Role to user. + """Grants the Role to user. Creates an entitlement. | - |OAuth authentication is required and the user needs to have access to the owner view.""", + |Authentication is required and the user needs to be a Super Admin. Super Admins are listed in the Props file.""", Extraction.decompose(CreateEntitlementJSON("obp-bank-x-gh", "CanQueryOtherUser")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1659,7 +1659,7 @@ trait APIMethods200 { "getEntitlements", "GET", "/users/USER_ID/entitlements", - "Get Entitlement specified by USER_ID", + "Get Entitlements specified by USER_ID", """ | |Login is required. @@ -1697,21 +1697,22 @@ trait APIMethods200 { "elasticSearchWarehouse", "GET", "/search", - "Elastic Search Warehouse", + "Search Warehouse Data", """ + |Search warehouse data via Elastic Search | |Login is required. | - |CanSearchWarehouse entitlement is requred for logged-in user. + |CanSearchWarehouse entitlement is requred for the logged-in user. | | """.stripMargin, emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, - true, - true, - true, + false, + false, + false, List()) val es = elasticsearchWarehouse diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 1e4d59203..e353bee2b 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -172,7 +172,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getCurrentUser, Implementations2_0_0.createUserCustomerLinks, Implementations2_0_0.addEntitlements, - Implementations2_0_0.getEntitlements + Implementations2_0_0.getEntitlements, + Implementations2_0_0.elasticSearchWarehouse ) routes.foreach(route => { From 287422919f838ec0dfdbc6b92e0173be16e52263 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 7 Jun 2016 19:30:24 +0200 Subject: [PATCH 501/702] Fixed elasticsearch warehouse endpoint and helpers --- .../scala/code/api/v2_0_0/APIMethods200.scala | 8 ++--- src/main/scala/code/search/search.scala | 35 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 33960d872..c51c8439d 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1718,14 +1718,14 @@ trait APIMethods200 { val es = elasticsearchWarehouse lazy val elasticSearchWarehouse: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "search" :: Nil JsonGet json => { + case "search" :: queryString :: Nil JsonGet _ => { user => for { u <- user ?~ ErrorMessages.UserNotLoggedIn - entitlements <- Entitlements.entitlementProvider.vend.getEntitlements(u.userId) - hasEntitlement <- booleanToBox(entitlements.contains(ApiRole.CanSearchWarehouse)) ?~ "User is not allowed to search warehouse!" + b <- Bank.all.headOption //TODO: This is a temp workaround + canSearchWarehouse <- Entitlements.entitlementProvider.vend.getEntitlement(b.bankId.toString, u.userId, ApiRole.CanSearchWarehouse.toString) ?~ "User is not allowed to search warehouse!" } yield { - successJsonResponse(Extraction.decompose(es.searchProxy(json.toString))) + successJsonResponse(Extraction.decompose(es.searchProxy(queryString))) } } } diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala index 45dcf6695..a036a84f7 100644 --- a/src/main/scala/code/search/search.scala +++ b/src/main/scala/code/search/search.scala @@ -23,9 +23,9 @@ import net.liftweb.util.Helpers import net.liftweb.util.Props import dispatch._ import Defaults._ +import kafka.utils.Json import net.liftweb.json - class elasticsearch { case class QueryStrings(esType: String, queryStr: String) @@ -45,31 +45,36 @@ class elasticsearch { , Duration.Inf) } - def searchProxy(query: String): LiftResponse = { - val request = constructQuery(getParametersFromUrl) + def searchProxy(queryString: String): LiftResponse = { + val request = constructQuery(getParameters(queryString)) val response = getAPIResponse(request) JsonResponse(response.body, ("Access-Control-Allow-Origin","*") :: Nil, Nil, response.code) } - private def constructQuery(queries: QueryStrings): Req = { - val jsonQuery = s"""{"query": ${queries.queryStr}}""" - val esUrl = Helpers.appendParams(s"$esHost/$esIndex/${queries.esType}/_search", Seq(("source",jsonQuery))) + private def constructQuery(params: Map[String, String]): Req = { + val esType = params.getOrElse("esType", "") + val filteredParams = params -- Set("esType") + val jsonQuery = Json.encode(filteredParams) + val esUrl = Helpers.appendParams( s"${esHost}/${esIndex}/${esType}${if (esType.nonEmpty) "/" else ""}_search", Seq(("source", jsonQuery))) url(esUrl).GET } - def getParametersFromUrl: QueryStrings = { - val query: Box[Map[String, List[String]]] = S.request.map(_.params) - val esType = S.param("dataset").getOrElse("") - val queryStr = S.param("query").getOrElse("") - QueryStrings(esType, queryStr) + def getParameters(queryString: String): Map[String, String] = { + val res = queryString.split('&') map { str => + val pair = str.split('=') + if (pair.length > 1) + (pair(0) -> pair(1)) + else + (pair(0) -> "") + } toMap + + res } } - - class elasticsearchLocal extends elasticsearch { override val esHost = Props.get("es_warehouse.host","http://localhost:9200") @@ -110,13 +115,13 @@ class elasticsearchLocal extends elasticsearch { object elasticsearchWarehouse extends elasticsearch { override val esHost = Props.get("es_warehouse.host","http://localhost:9200") - override val esIndex = Props.get("es_warehouse.data_index","es_warehouse_data_index") + override val esIndex = Props.get("es_warehouse.data_index","warehouse") } class elasticsearchMetrics extends elasticsearch { override val esHost = Props.get("es_metrics.host","http://localhost:9200") - override val esIndex = Props.get("es_metrics.data_index","es_metrics_data_index") + override val esIndex = Props.get("es_metrics.data_index","metrics") } From 5555f5ade5af2714394b323a9aba8f198ebbfdb8 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 8 Jun 2016 09:39:32 +0200 Subject: [PATCH 502/702] More fixes to elasticsearch warehouse endpoint and helpers --- src/main/scala/code/api/util/APIUtil.scala | 4 +- src/main/scala/code/metrics/APIMetrics.scala | 2 +- .../code/metrics/ElasticsearchMetrics.scala | 31 ++++++ src/main/scala/code/search/search.scala | 99 +++++++++++++------ 4 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 src/main/scala/code/metrics/ElasticsearchMetrics.scala diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index cd0864fea..e993a54aa 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -151,7 +151,9 @@ object APIUtil extends Loggable { Empty } // TODO This should use Elastic Search or Kafka not an RDBMS - APIMetrics.apiMetrics.vend.saveMetric(user.orNull.userId, S.uriAndQueryString.getOrElse(""), (now: TimeSpan)) + val u = user.orNull + val userId = if (u != null) u.userId else "null" + APIMetrics.apiMetrics.vend.saveMetric(userId, S.uriAndQueryString.getOrElse(""), (now: TimeSpan)) } } diff --git a/src/main/scala/code/metrics/APIMetrics.scala b/src/main/scala/code/metrics/APIMetrics.scala index 823cfcbc2..76f36e0d1 100644 --- a/src/main/scala/code/metrics/APIMetrics.scala +++ b/src/main/scala/code/metrics/APIMetrics.scala @@ -7,7 +7,7 @@ object APIMetrics extends SimpleInjector { val apiMetrics = new Inject(buildOne _) {} - def buildOne: APIMetrics = MappedMetrics + def buildOne: APIMetrics = ElasticsearchMetrics /** * Returns a Date which is at the start of the day of the date diff --git a/src/main/scala/code/metrics/ElasticsearchMetrics.scala b/src/main/scala/code/metrics/ElasticsearchMetrics.scala new file mode 100644 index 000000000..5adcc2dd1 --- /dev/null +++ b/src/main/scala/code/metrics/ElasticsearchMetrics.scala @@ -0,0 +1,31 @@ +package code.metrics + +import java.util.Date + +import code.search.elasticsearchMetrics + +object ElasticsearchMetrics extends APIMetrics { + + val es = new elasticsearchMetrics + + override def saveMetric(userId: String, url: String, date: Date): Unit = { + es.indexMetric(userId, url, date) + MappedMetric.create.url(url).date(date).save + } + + override def getAllGroupedByUserId(): Map[String, List[APIMetric]] = { + //TODO: do this all at the db level using an actual group by query + MappedMetric.findAll.groupBy(_.getUserId) + } + + override def getAllGroupedByDay(): Map[Date, List[APIMetric]] = { + //TODO: do this all at the db level using an actual group by query + MappedMetric.findAll.groupBy(APIMetrics.getMetricDay) + } + + override def getAllGroupedByUrl(): Map[String, List[APIMetric]] = { + //TODO: do this all at the db level using an actual group by query + MappedMetric.findAll.groupBy(_.getUrl()) + } + +} diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala index a036a84f7..6c6ed1a59 100644 --- a/src/main/scala/code/search/search.scala +++ b/src/main/scala/code/search/search.scala @@ -25,6 +25,11 @@ import dispatch._ import Defaults._ import kafka.utils.Json import net.liftweb.json +import java.util.Date + +import com.sksamuel.elastic4s.analyzers.StopAnalyzer +import com.sksamuel.elastic4s.mappings.FieldType.{DateType, ObjectType, StringType} +import com.sksamuel.elastic4s.source.Indexable class elasticsearch { @@ -33,7 +38,6 @@ class elasticsearch { case class ErrorMessage(error: String) val esHost = "" - val esIndex = "" def getAPIResponse(req: Req): APIResponse = { Await.result( @@ -53,9 +57,11 @@ class elasticsearch { private def constructQuery(params: Map[String, String]): Req = { val esType = params.getOrElse("esType", "") - val filteredParams = params -- Set("esType") + val esIndex = params.getOrElse("esIndex", "") + val filteredParams = params -- Set("esIndex", "esType") val jsonQuery = Json.encode(filteredParams) - val esUrl = Helpers.appendParams( s"${esHost}/${esIndex}/${esType}${if (esType.nonEmpty) "/" else ""}_search", Seq(("source", jsonQuery))) + val httpHost = ("http://" + esHost).replaceAll("9300", "9200") + val esUrl = Helpers.appendParams( s"${httpHost}/${esIndex}/${esType}${if (esType.nonEmpty) "/" else ""}_search", Seq(("source", jsonQuery))) url(esUrl).GET } @@ -74,18 +80,63 @@ class elasticsearch { } +object elasticsearchWarehouse extends elasticsearch { + override val esHost = Props.get("es.warehouse.host","localhost:9300") + val client = ElasticClient.transport("elasticsearch://" + esHost + ",") +} -class elasticsearchLocal extends elasticsearch { - override val esHost = Props.get("es_warehouse.host","http://localhost:9200") - override val esIndex = Props.get("es_warehouse.data_index","es_warehouse_data_index") +class elasticsearchMetrics extends elasticsearch { + override val esHost = Props.get("es.metrics.host","localhost:9300") + val client = ElasticClient.transport("elasticsearch://" + esHost + ",") + val metricsIndex = Props.get("es.metrics.index","metrics") + client.execute { create index metricsIndex } + client.execute { + create index metricsIndex mappings ( + "request" as ( + "userId" typed StringType, + "url" typed StringType, + "date" typed DateType + ) + ) + } - val settings = Settings.settingsBuilder() - .put("http.enabled", true) - .put("path.home", "/tmp/elastic/") + def indexMetric(userId: String, url: String, date: Date) { + client.execute { + index into metricsIndex / "request" fields ( + "userId" -> userId, + "url" -> url, + "date" -> date + ) + } + } +} - val client = ElasticClient.local(settings.build) +class elasticsearchOBP extends elasticsearch { + override val esHost = Props.get("es.obp.host","localhost:9300") + val client = ElasticClient.transport("elasticsearch://" + esHost + ",") + + val accountIndex = "account_v1.2.1" + val transactionIndex = "transaction_v1.2.1" + + client.execute { + create index accountIndex mappings ( + "account" as ( + "viewId" typed StringType, + "account" typed ObjectType + ) + ) + } + + client.execute { + create index transactionIndex mappings ( + "transaction" as ( + "viewId" typed StringType, + "transaction" typed ObjectType + ) + ) + } /* Index objects in Elastic Search. Use **the same** representations that we return in the REST API. @@ -96,32 +147,24 @@ class elasticsearchLocal extends elasticsearch { // Put into a index that has the viewId and version in the name. def indexTransaction(viewId: String, transaction: TransactionJSON) { client.execute { - index into "transaction_v1.2.1" fields { - viewId -> transaction - } + index into transactionIndex / "transaction" fields ( + "viewId" -> viewId, + "transaction" -> transaction + ) } } + // Index an Account + // Put into a index that has the viewId and version in the name. def indexAccount(viewId: String, account: AccountJSON) { client.execute { - index into "account_v1.2.1" fields { - viewId -> account - } + index into accountIndex / "account" fields ( + "viewId" -> viewId, + "account" -> account + ) } } } - object elasticsearchWarehouse extends elasticsearch { - override val esHost = Props.get("es_warehouse.host","http://localhost:9200") - override val esIndex = Props.get("es_warehouse.data_index","warehouse") - } - - - class elasticsearchMetrics extends elasticsearch { - override val esHost = Props.get("es_metrics.host","http://localhost:9200") - override val esIndex = Props.get("es_metrics.data_index","metrics") - } - - From ba5f52cd4ac5789f07f1ffb9b0ac41b76a9d6554 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 8 Jun 2016 10:52:48 +0200 Subject: [PATCH 503/702] Fixed CSS for unicredit --- src/main/webapp/media/css/overrides/unicredit.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/webapp/media/css/overrides/unicredit.css b/src/main/webapp/media/css/overrides/unicredit.css index d9db3e99d..1d575b2e3 100644 --- a/src/main/webapp/media/css/overrides/unicredit.css +++ b/src/main/webapp/media/css/overrides/unicredit.css @@ -1,4 +1,6 @@ #header-decoration, #authorizeSection { background-color: #d60006; +} +#header-decoration { height: 4px; } From f6edcfcb4d8a0e6870a359096654f642d96e5b3a Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 8 Jun 2016 13:05:49 +0200 Subject: [PATCH 504/702] Found more boxes to color properly for unicredit --- src/main/webapp/media/css/overrides/unicredit.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/media/css/overrides/unicredit.css b/src/main/webapp/media/css/overrides/unicredit.css index 1d575b2e3..1043ea6d8 100644 --- a/src/main/webapp/media/css/overrides/unicredit.css +++ b/src/main/webapp/media/css/overrides/unicredit.css @@ -1,4 +1,7 @@ -#header-decoration, #authorizeSection { +#header-decoration, +#authorizeSection, +#registerAppSection, +#create-sandbox-account { background-color: #d60006; } #header-decoration { From 9f3d5ec832f916cec09a0386026044e58f3b95b1 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Wed, 8 Jun 2016 13:06:17 +0200 Subject: [PATCH 505/702] Changed landing page to include descriptions of API capabilities --- src/main/webapp/index.html | 99 ++++++++++++++++++++++++++- src/main/webapp/media/css/website.css | 87 +++++++++++++++++------ 2 files changed, 162 insertions(+), 24 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 9e801ba2d..9d36c8aad 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -1,6 +1,6 @@ " + esHost + ":" + esPortHTTP + "/" + esIndex + "/" + queryString) + if (Props.getBool("allow_elasticsearch", false) ) { + val request = constructQuery(getParameters(queryString)) + val response = getAPIResponse(request) + JsonResponse(compactRender(response.body), ("Access-Control-Allow-Origin", "*") :: Nil, Nil, response.code) + } else { + JsonResponse(json.JsonParser.parse("""{"error":"elasticsearch disabled"}"""), ("Access-Control-Allow-Origin", "*") :: Nil, Nil, 404) + } + } + + private def getAPIResponse(req: Req): APIResponse = { Await.result( for (response <- Http(req > as.Response(p => p))) yield { @@ -50,12 +51,6 @@ class elasticsearch { , Duration.Inf) } - def searchProxy(queryString: String): LiftResponse = { - val request = constructQuery(getParameters(queryString)) - val response = getAPIResponse(request) - JsonResponse(compactRender(response.body), ("Access-Control-Allow-Origin","*") :: Nil, Nil, response.code) - } - private def constructQuery(params: Map[String, String]): Req = { val esType = params.getOrElse("esType", "") val q = params.getOrElse("q", "") @@ -63,7 +58,7 @@ class elasticsearch { val filteredParams = params -- Set("esIndex", "esType") val jsonQuery = Json.encode(filteredParams) //TODO: Workaround - HTTP and TCP ports differ. Should there be props entry for both? - val httpHost = ("http://" + esHost).replaceAll("9300", "9200") + val httpHost = ("http://" + esHost + ":" + esPortHTTP) val esUrl = if (q != "") Helpers.appendParams( s"${httpHost}/${esIndex}/${esType}${if (esType.nonEmpty) "/" else ""}_search", Seq(("q", q))) @@ -75,7 +70,7 @@ class elasticsearch { url(esUrl).GET } - def getParameters(queryString: String): Map[String, String] = { + private def getParameters(queryString: String): Map[String, String] = { val res = queryString.split('&') map { str => val pair = str.split('=') if (pair.length > 1) @@ -90,67 +85,84 @@ class elasticsearch { } -object elasticsearchMetrics extends elasticsearch { - override val esHost = Props.get("es.metrics.host","localhost:9300") - override val esIndex = Props.get("es.metrics.index", "") +class elasticsearchMetrics extends elasticsearch { + override val esHost = Props.get("es.metrics.host","localhost") + override val esPortTCP = Props.get("es.metrics.port.tcp","9300") + override val esPortHTTP = Props.get("es.metrics.port.tcp","9200") + override val esIndex = Props.get("es.metrics.index", "metrics") - val client = ElasticClient.transport("elasticsearch://" + esHost + ",") + var client:ElasticClient = null - val metricsIndex = Props.get("es.metrics.index","metrics") - client.execute { create index metricsIndex } - client.execute { - create index metricsIndex mappings ( - "request" as ( - "userId" typed StringType, - "url" typed StringType, - "date" typed DateType + if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { + client = ElasticClient.transport("elasticsearch://" + esHost + ":" + esPortTCP + ",") + client.execute { create index esIndex } + client.execute { + create index esIndex mappings ( + "request" as ( + "userId" typed StringType, + "url" typed StringType, + "date" typed DateType + ) ) - ) + } } def indexMetric(userId: String, url: String, date: Date) { + if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { client.execute { - index into metricsIndex / "request" fields ( + index into esIndex / "request" fields ( "userId" -> userId, "url" -> url, "date" -> date ) } } + } } -object elasticsearchWarehouse extends elasticsearch { - override val esHost = Props.get("es.warehouse.host","localhost:9300") - override val esIndex = Props.get("es.warehouse.index", "") - val client = ElasticClient.transport("elasticsearch://" + esHost + ",") +class elasticsearchWarehouse extends elasticsearch { + override val esHost = Props.get("es.warehouse.host","localhost") + override val esPortTCP = Props.get("es.warehouse.port.tcp","9300") + override val esPortHTTP = Props.get("es.warehouse.port.tcp","9200") + override val esIndex = Props.get("es.warehouse.index", "warehouse") + var client:ElasticClient = null + if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_warehouse", false) ) { + client = ElasticClient.transport("elasticsearch://" + esHost + ":" + esPortTCP + ",") + } } - -object elasticsearchOBP extends elasticsearch { - override val esHost = Props.get("es.obp.host","localhost:9300") - override val esIndex = Props.get("es.obp.index", "") - val client = ElasticClient.transport("elasticsearch://" + esHost + ",") - +/* +class elasticsearchOBP extends elasticsearch { + override val esHost = Props.get("es.obp.host","localhost") + override val esPortTCP = Props.get("es.obp.port.tcp","9300") + override val esPortHTTP = Props.get("es.obp.port.tcp","9200") + override val esIndex = Props.get("es.obp.index", "obp") val accountIndex = "account_v1.2.1" val transactionIndex = "transaction_v1.2.1" - client.execute { - create index accountIndex mappings ( - "account" as ( - "viewId" typed StringType, - "account" typed ObjectType - ) - ) - } + var client:ElasticClient = null - client.execute { - create index transactionIndex mappings ( - "transaction" as ( - "viewId" typed StringType, - "transaction" typed ObjectType + if (Props.getBool("allow_elasticsearch", false) ) { + client = ElasticClient.transport("elasticsearch://" + esHost + ":" + esPortTCP + ",") + + client.execute { + create index accountIndex mappings ( + "account" as ( + "viewId" typed StringType, + "account" typed ObjectType + ) ) - ) + } + + client.execute { + create index transactionIndex mappings ( + "transaction" as ( + "viewId" typed StringType, + "transaction" typed ObjectType + ) + ) + } } /* Index objects in Elastic Search. @@ -161,25 +173,29 @@ object elasticsearchOBP extends elasticsearch { // Index a Transaction // Put into a index that has the viewId and version in the name. def indexTransaction(viewId: String, transaction: TransactionJSON) { - client.execute { - index into transactionIndex / "transaction" fields ( - "viewId" -> viewId, - "transaction" -> transaction - ) + if (Props.getBool("allow_elasticsearch", false) ) { + client.execute { + index into transactionIndex / "transaction" fields ( + "viewId" -> viewId, + "transaction" -> transaction + ) + } } } // Index an Account // Put into a index that has the viewId and version in the name. def indexAccount(viewId: String, account: AccountJSON) { - client.execute { - index into accountIndex / "account" fields ( - "viewId" -> viewId, - "account" -> account - ) + if (Props.getBool("allow_elasticsearch", false) ) { + client.execute { + index into accountIndex / "account" fields ( + "viewId" -> viewId, + "account" -> account + ) + } } } } - +*/ From ebe7af1e82a560f65b34c658c3d8098dc2b855cb Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 9 Jun 2016 19:00:04 +0200 Subject: [PATCH 511/702] Comment cleanup --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 33552f691..2ca1db023 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1718,11 +1718,11 @@ trait APIMethods200 { | | example usage: | - | /warehouse/search/q=findThis + | /search/warehouse/q=findThis | |or: | - | /warehouse/search/source={"query":{"query_string":{"query":"findThis"}}} + | /search/warehouse/source={"query":{"query_string":{"query":"findThis"}}} | | |Note: @@ -1781,11 +1781,11 @@ trait APIMethods200 { | | example usage: | - | /metrics/search/q=findThis + | /search/metrics/q=findThis | |or: | - | /metrics/search/source={"query":{"query_string":{"query":"findThis"}}} + | /search/metrics/source={"query":{"query_string":{"query":"findThis"}}} | | |Note: From d00736675ff88cfa8d882e10363e4cfbe40858e0 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 9 Jun 2016 23:02:08 +0100 Subject: [PATCH 512/702] comment on old Roles --- src/main/scala/code/model/dataAccess/APIUser.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/model/dataAccess/APIUser.scala b/src/main/scala/code/model/dataAccess/APIUser.scala index 0470dcd2e..310e577bf 100644 --- a/src/main/scala/code/model/dataAccess/APIUser.scala +++ b/src/main/scala/code/model/dataAccess/APIUser.scala @@ -88,8 +88,7 @@ class APIUser extends LongKeyedMapper[APIUser] with User with ManyToMany with On def provider = provider_.get def views: List[View] = views_.toList - // Roles interface - // Not sure if this is a good API since the list will grow. + // Depreciated. Do not use./////////////// def isCrmAdmin : Boolean = hasCrmAdminRole def isCrmReader : Boolean = hasCrmReaderRole def isCustomerMessageAdmin : Boolean = hasCustomerMessageAdminRole @@ -97,7 +96,7 @@ class APIUser extends LongKeyedMapper[APIUser] with User with ManyToMany with On def isBranchReader : Boolean = hasBranchReaderRole def isAtmReader : Boolean = hasAtmReaderRole def isProductReader : Boolean = hasProductReaderRole - // + //////////////////////////////////////////////////// } object APIUser extends APIUser with LongKeyedMetaMapper[APIUser]{ From 50ab1353ada9988e9c9d21f8d2d5a6c6b1e1ce58 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 10 Jun 2016 08:45:35 +0200 Subject: [PATCH 513/702] Add API call to Link User to Customer #35 - added primary key, removed BANK_ID from url and table --- src/main/scala/code/api/util/APIUtil.scala | 1 + .../scala/code/api/v2_0_0/APIMethods200.scala | 21 ++++++++++--------- .../code/api/v2_0_0/JSONFactory2.0.0.scala | 10 +++++---- .../code/customer/CustomerProvider.scala | 2 ++ .../customer/MappedCustomerProvider.scala | 6 ++++++ .../MappedUserCustomerLinkProvider.scala | 14 +++++-------- .../usercustomerlinks/UserCustomerLink.scala | 4 ++-- .../scala/code/api/v1_4_0/CustomerTest.scala | 7 +++++++ 8 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index e993a54aa..7d76d61bd 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -81,6 +81,7 @@ object ErrorMessages { // Resource related messages val BankNotFound = "OBP-30001: Bank not found. Please specify a valid value for BANK_ID." val CustomerNotFound = "OBP-30002: Customer not found. Please specify a valid value for CUSTOMER_NUMBER." + val CustomerNotFoundByCustomerId = "OBP-30002: Customer not found. Please specify a valid value for CUSTOMER_ID." val AccountNotFound = "OBP-30003: Account not found. Please specify a valid value for ACCOUNT_ID." val CounterpartyNotFound = "OBP-30004: Counterparty not found. The BANK_ID / ACCOUNT_ID specified does not exist on this server." diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 2ca1db023..446e3af62 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1584,14 +1584,14 @@ trait APIMethods200 { apiVersion, "createUserCustomerLinks", "POST", - "/banks/BANK_ID/UserCustomerLinks", + "/banks/user_customer_links", "Create user customer link.", """Link a customer and an user |This call may require additional permissions/role in the future. |For now the authenticated user can create at most one linked customer. |OAuth authentication is required. |""", - emptyObjectJson, + Extraction.decompose(CreateUserCustomerLinkJSON("be106783-b4fa-48e6-b102-b178a11a8e9b", "02141bc6-0a69-4fba-b4db-a17e5fbbbdcc")), emptyObjectJson, emptyObjectJson :: Nil, false, @@ -1600,16 +1600,17 @@ trait APIMethods200 { List(apiTagCustomer)) lazy val createUserCustomerLinks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "UserCustomerLinks" :: Nil JsonPost json -> _ => { + case "banks" :: "user_customer_links" :: Nil JsonPost json -> _ => { user => for { - u <- user ?~! "User must be logged in to post Customer" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - user_id <- tryo(u.userId.isEmpty) ?~! "Field user_id is not defined for the logged user!" - customer_user <- User.findByUserId(u.userId) ?~! ErrorMessages.UserNotFoundById - customer <- tryo(Customer.customerProvider.vend.getCustomer(bankId, customer_user).get) ?~! ErrorMessages.CustomerNotFound - userCustomerLink <- booleanToBox(UserCustomerLink.userCustomerLinkProvider.vend.getUserCustomerLink(u.userId, customer.customerId).isEmpty == true) ?~ ErrorMessages.CustomerAlreadyExistsForUser - userCustomerLink <- UserCustomerLink.userCustomerLinkProvider.vend.createUserCustomerLink(u.userId, customer.customerId, bankId.value.toString, exampleDate, true) ?~! "Could not create userCustomerLink" + u <- user ?~! "User must be logged in to post user customer link" + postedData <- tryo{json.extract[CreateUserCustomerLinkJSON]} ?~! ErrorMessages.InvalidJsonFormat + user_id <- booleanToBox(postedData.user_id.nonEmpty) ?~ "Field user_id is not defined in the posted json!" + user <- User.findByUserId(postedData.user_id) ?~! ErrorMessages.UserNotFoundById + customer_id <- booleanToBox(postedData.customer_id.nonEmpty) ?~ "Field customer_id is not defined in the posted json!" + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(postedData.customer_id) ?~ ErrorMessages.CustomerNotFoundByCustomerId + userCustomerLink <- booleanToBox(UserCustomerLink.userCustomerLinkProvider.vend.getUserCustomerLink(postedData.user_id, postedData.customer_id).isEmpty == true) ?~ ErrorMessages.CustomerAlreadyExistsForUser + userCustomerLink <- UserCustomerLink.userCustomerLinkProvider.vend.createUserCustomerLink(postedData.user_id, postedData.customer_id, exampleDate, true) ?~! "Could not create user_customer_links" } yield { val successJson = Extraction.decompose(code.api.v2_0_0.JSONFactory200.createUserCustomerLinkJSON(userCustomerLink)) successJsonResponse(successJson, 201) diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 960b4d16a..a808c9442 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -112,13 +112,15 @@ case class MeetingPresentJSON( ) -case class UserCustomerLinkJSON(customer_id: String, +case class UserCustomerLinkJSON(user_customer_link_id: String, + customer_id: String, user_id: String, - bank_id: String, date_inserted: Date, is_active: Boolean) case class UserCustomerLinkJSONs(l: List[UserCustomerLinkJSON]) +case class CreateUserCustomerLinkJSON(user_id: String, customer_id: String) + class BasicViewJSON( val id: String, val short_name: String, @@ -759,9 +761,9 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy } def createUserCustomerLinkJSON(ucl: code.usercustomerlinks.UserCustomerLink) = { - UserCustomerLinkJSON(customer_id = ucl.customerId, + UserCustomerLinkJSON(user_customer_link_id = ucl.userCustomerLinkId, + customer_id = ucl.customerId, user_id = ucl.userId, - bank_id = ucl.bankId, date_inserted = ucl.dateInserted, is_active = ucl.isActive ) diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala index 9bb06c3ac..054bedb8a 100644 --- a/src/main/scala/code/customer/CustomerProvider.scala +++ b/src/main/scala/code/customer/CustomerProvider.scala @@ -17,6 +17,8 @@ object Customer extends SimpleInjector { trait CustomerProvider { def getCustomer(bankId : BankId, user : User) : Box[Customer] + def getCustomerByCustomerId(customerId: String): Box[Customer] + def getUser(bankId : BankId, customerNumber : String) : Box[User] def checkCustomerNumberAvailable(bankId : BankId, customerNumber : String) : Boolean diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 9c0c1cdb4..247391cc1 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -31,6 +31,12 @@ object MappedCustomerProvider extends CustomerProvider { By(MappedCustomer.mBank, bankId.value)) } + override def getCustomerByCustomerId(customerId: String): Box[Customer] = { + MappedCustomer.find( + By(MappedCustomer.mCustomerId, customerId) + ) + } + override def getUser(bankId: BankId, customerNumber: String): Box[User] = { MappedCustomer.find( By(MappedCustomer.mBank, bankId.value), diff --git a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala index d35efa30b..e8c5eab1d 100644 --- a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala +++ b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala @@ -1,10 +1,7 @@ package code.usercustomerlinks import java.util.Date - -import code.customer.Customer -import code.model.{User, BankId} -import code.util.{DefaultStringField} +import code.util.{MappedUUID, DefaultStringField} import net.liftweb.common.Box import net.liftweb.mapper._ @@ -13,12 +10,11 @@ import net.liftweb.mapper._ */ object MappedUserCustomerLinkProvider extends UserCustomerLinkProvider { - override def createUserCustomerLink(userId: String, customerId: String, bankId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] = { + override def createUserCustomerLink(userId: String, customerId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] = { val createUserCustomerLink = MappedUserCustomerLink.create .mUserId(userId) .mCustomerId(customerId) - .mBankId(bankId) .mDateInserted(new Date()) .mIsActive(isActive) .saveMe() @@ -44,15 +40,15 @@ class MappedUserCustomerLink extends UserCustomerLink with LongKeyedMapper[Mappe // Name the objects m* so that we can give the overridden methods nice names. // Assume we'll have to override all fields so name them all m* + object mUserCustomerLinkId extends MappedUUID(this) object mCustomerId extends DefaultStringField(this) - object mBankId extends DefaultStringField(this) object mUserId extends DefaultStringField(this) object mDateInserted extends MappedDateTime(this) object mIsActive extends MappedBoolean(this) + override def userCustomerLinkId: String = mUserCustomerLinkId.get override def customerId: String = mCustomerId.get // id.toString override def userId: String = mUserId.get - override def bankId : String = mBankId.get override def dateInserted: Date = mDateInserted.get override def isActive: Boolean = mIsActive @@ -61,5 +57,5 @@ class MappedUserCustomerLink extends UserCustomerLink with LongKeyedMapper[Mappe } object MappedUserCustomerLink extends MappedUserCustomerLink with LongKeyedMetaMapper[MappedUserCustomerLink] { - override def dbIndexes = UniqueIndex(mUserId, mCustomerId) :: super.dbIndexes + override def dbIndexes = UniqueIndex(mUserCustomerLinkId) :: UniqueIndex(mUserId, mCustomerId) :: super.dbIndexes } diff --git a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala index 20975b09e..318a37b23 100644 --- a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala +++ b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala @@ -6,9 +6,9 @@ import net.liftweb.common.Box import net.liftweb.util.SimpleInjector trait UserCustomerLink { + def userCustomerLinkId: String def userId: String def customerId: String - def bankId: String def dateInserted: Date def isActive: Boolean } @@ -23,7 +23,7 @@ object UserCustomerLink extends SimpleInjector { } trait UserCustomerLinkProvider { - def createUserCustomerLink(userId: String, customerId: String, bankId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] + def createUserCustomerLink(userId: String, customerId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] def getUserCustomerLink(userId: String, customerId: String): Box[UserCustomerLink] def getUserCustomerLinks: Box[List[UserCustomerLink]] } \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index afbf795ce..fdee0d60e 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -14,6 +14,8 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { val mockBankId = BankId("testBank1") + val mocCustomerId = "ddfc0cf8-82a0-42eb-9027-8cfb0e7988c9" + case class MockFaceImage(date : Date, url : String) extends CustomerFaceImage case class MockCustomer(customerId: String, number: String, mobileNumber: String, @@ -36,6 +38,11 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { else Empty } + override def getCustomerByCustomerId(customerId: String): Box[Customer] = { + if(customerId == mocCustomerId) Full(mockCustomer) + else Empty + } + override def getUser(bankId: BankId, customerId: String): Box[User] = Empty override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage, dateOfBirth: Date, From ab3856c7313c2e4c565b538fc2fa777462dc7f8c Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 10 Jun 2016 17:17:17 +0200 Subject: [PATCH 514/702] Catch exception when elastic search not available --- src/main/scala/code/search/search.scala | 27 ++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala index e69caec13..e2e54b7c7 100644 --- a/src/main/scala/code/search/search.scala +++ b/src/main/scala/code/search/search.scala @@ -95,26 +95,35 @@ class elasticsearchMetrics extends elasticsearch { if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { client = ElasticClient.transport("elasticsearch://" + esHost + ":" + esPortTCP + ",") - client.execute { create index esIndex } - client.execute { - create index esIndex mappings ( + try { + client.execute { + create index esIndex mappings ( "request" as ( "userId" typed StringType, "url" typed StringType, "date" typed DateType ) ) + } + } + catch { + case e:Throwable => println("ERROR - "+ e.getMessage ) } } def indexMetric(userId: String, url: String, date: Date) { if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { - client.execute { - index into esIndex / "request" fields ( - "userId" -> userId, - "url" -> url, - "date" -> date - ) + try { + client.execute { + index into esIndex / "request" fields ( + "userId" -> userId, + "url" -> url, + "date" -> date + ) + } + } + catch { + case e:Throwable => println("ERROR - "+ e.getMessage ) } } } From 5e6819377ca408b4e110639e0317be533c04ffb5 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 10 Jun 2016 19:39:15 +0200 Subject: [PATCH 515/702] Removed clashing UniqueIndex entries --- src/main/scala/code/customer/MappedCustomerProvider.scala | 2 +- .../code/usercustomerlinks/MappedUserCustomerLinkProvider.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 247391cc1..94775e036 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -113,5 +113,5 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with object MappedCustomer extends MappedCustomer with LongKeyedMetaMapper[MappedCustomer] { //one customer info per bank for each api user - override def dbIndexes = UniqueIndex(mBank, mNumber) :: UniqueIndex(mUser, mBank) :: super.dbIndexes + override def dbIndexes = UniqueIndex(mCustomerId) :: super.dbIndexes } \ No newline at end of file diff --git a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala index e8c5eab1d..00a8e0394 100644 --- a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala +++ b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLinkProvider.scala @@ -57,5 +57,5 @@ class MappedUserCustomerLink extends UserCustomerLink with LongKeyedMapper[Mappe } object MappedUserCustomerLink extends MappedUserCustomerLink with LongKeyedMetaMapper[MappedUserCustomerLink] { - override def dbIndexes = UniqueIndex(mUserCustomerLinkId) :: UniqueIndex(mUserId, mCustomerId) :: super.dbIndexes + override def dbIndexes = UniqueIndex(mUserCustomerLinkId) :: super.dbIndexes } From a8666fd577b08ec6f108d24dd7d9c1f8c0ebff69 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 11 Jun 2016 15:08:03 +0100 Subject: [PATCH 516/702] Tweaking index.html text --- src/main/webapp/index.html | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 9d36c8aad..3fe2f62be 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -1,6 +1,6 @@ " + esHost + ":" + esPortHTTP + "/" + esIndex + "/" + queryString) if (Props.getBool("allow_elasticsearch", false) ) { - val request = constructQuery(getParameters(queryString)) + val request = constructQuery(userId, getParameters(queryString)) val response = getAPIResponse(request) ESJsonResponse(response.body, ("Access-Control-Allow-Origin", "*") :: Nil, Nil, response.code) } else { @@ -61,7 +61,7 @@ class elasticsearch extends Loggable { , Duration.Inf) } - private def constructQuery(params: Map[String, String]): Req = { + private def constructQuery(userId: String, params: Map[String, String]): Req = { val esType = params.getOrElse("esType", "") val q = params.getOrElse("q", "") val source = params.getOrElse("source","") @@ -93,7 +93,7 @@ class elasticsearch extends Loggable { println("[ES.URL]===> " + esUrl) // Use this incase we cant log to elastic search - logger.info(s"esUrl is $esUrl parameters are $parameters user_id is: TODO ") + logger.info(s"esUrl is $esUrl parameters are $parameters user_id is $userId") url(esUrl).GET } From 27a29bb77335d689040120f765792fe815b18ef5 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 17 Jun 2016 12:24:21 +0200 Subject: [PATCH 540/702] Fixed problem opening too many connections to elasticsearch --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 13 +++++-------- src/main/scala/code/metrics/APIMetrics.scala | 9 +++++---- .../scala/code/metrics/ElasticsearchMetrics.scala | 11 ++++++----- src/main/scala/code/search/search.scala | 2 ++ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 45052575f..dbe1a791b 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1772,8 +1772,8 @@ trait APIMethods200 { } } - //if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_warehouse", false) ) { - resourceDocs += ResourceDoc( + // TODO Put message into doc below if not enabled (but continue to show API Doc) + resourceDocs += ResourceDoc( elasticSearchWarehouse, apiVersion, "elasticSearchWarehouse", @@ -1851,8 +1851,8 @@ trait APIMethods200 { false, false, List()) - //} + val esw = new elasticsearchWarehouse lazy val elasticSearchWarehouse: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "search" :: "warehouse" :: queryString :: Nil JsonGet _ => { user => @@ -1861,15 +1861,13 @@ trait APIMethods200 { b <- Bank.all.headOption //TODO: This is a temp workaround canSearchWarehouse <- Entitlement.entitlement.vend.getEntitlement(b.bankId.toString, u.userId, ApiRole.CanSearchWarehouse.toString) ?~ "CanSearchWarehouse entitlement required" } yield { - val esw = new elasticsearchWarehouse successJsonResponse(Extraction.decompose(esw.searchProxy(u.userId, queryString))) } } } // TODO Put message into doc below if not enabled (but continue to show API Doc) - // if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { - resourceDocs += ResourceDoc( + resourceDocs += ResourceDoc( elasticSearchMetrics, apiVersion, "elasticSearchMetrics", @@ -1942,8 +1940,8 @@ trait APIMethods200 { false, false, List()) - //} + val esm = new elasticsearchMetrics lazy val elasticSearchMetrics: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "search" :: "metrics" :: queryString :: Nil JsonGet _ => { user => @@ -1952,7 +1950,6 @@ trait APIMethods200 { b <- Bank.all.headOption //TODO: This is a temp workaround canSearchMetrics <- Entitlement.entitlement.vend.getEntitlement(b.bankId.toString, u.userId, ApiRole.CanSearchMetrics.toString) ?~ "CanSearchMetrics entitlement required" } yield { - val esm = new elasticsearchMetrics successJsonResponse(Extraction.decompose(esm.searchProxy(u.userId, queryString))) } } diff --git a/src/main/scala/code/metrics/APIMetrics.scala b/src/main/scala/code/metrics/APIMetrics.scala index 3774e79e2..417b34c89 100644 --- a/src/main/scala/code/metrics/APIMetrics.scala +++ b/src/main/scala/code/metrics/APIMetrics.scala @@ -7,10 +7,11 @@ object APIMetrics extends SimpleInjector { val apiMetrics = new Inject(buildOne _) {} - def buildOne: APIMetrics = ElasticsearchMetrics - Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) match { - case false => MappedMetrics - case true => ElasticsearchMetrics + def buildOne: APIMetrics = + Props.getBool("allow_elasticsearch", false) && + Props.getBool("allow_elasticsearch_metrics", false) match { + case false => MappedMetrics + case true => ElasticsearchMetrics } /** diff --git a/src/main/scala/code/metrics/ElasticsearchMetrics.scala b/src/main/scala/code/metrics/ElasticsearchMetrics.scala index 2986fb890..937fa59b2 100644 --- a/src/main/scala/code/metrics/ElasticsearchMetrics.scala +++ b/src/main/scala/code/metrics/ElasticsearchMetrics.scala @@ -7,26 +7,27 @@ import net.liftweb.util.Props object ElasticsearchMetrics extends APIMetrics { + val es = new elasticsearchMetrics + override def saveMetric(userId: String, url: String, date: Date): Unit = { if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { - val es = new elasticsearchMetrics + es.indexMetric(userId, url, date) } - MappedMetric.create.url(url).date(date).save } override def getAllGroupedByUserId(): Map[String, List[APIMetric]] = { - //TODO: do this all at the db level using an actual group by query + //TODO: replace the following with valid ES query MappedMetric.findAll.groupBy(_.getUserId) } override def getAllGroupedByDay(): Map[Date, List[APIMetric]] = { - //TODO: do this all at the db level using an actual group by query + //TODO: replace the following with valid ES query MappedMetric.findAll.groupBy(APIMetrics.getMetricDay) } override def getAllGroupedByUrl(): Map[String, List[APIMetric]] = { - //TODO: do this all at the db level using an actual group by query + //TODO: replace the following with valid ES query MappedMetric.findAll.groupBy(_.getUrl()) } diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala index faf73044e..effd36585 100644 --- a/src/main/scala/code/search/search.scala +++ b/src/main/scala/code/search/search.scala @@ -114,6 +114,7 @@ class elasticsearch extends Loggable { class elasticsearchMetrics extends elasticsearch { + //println("-----------------------------------------------------> elasticsearchMetrics instantiated") override val esHost = Props.get("es.metrics.host","localhost") override val esPortTCP = Props.get("es.metrics.port.tcp","9300") override val esPortHTTP = Props.get("es.metrics.port.http","9200") @@ -157,6 +158,7 @@ class elasticsearchMetrics extends elasticsearch { } class elasticsearchWarehouse extends elasticsearch { + //println("-----------------------------------------------------> elasticsearchWarehouse instantiated") override val esHost = Props.get("es.warehouse.host","localhost") override val esPortTCP = Props.get("es.warehouse.port.tcp","9300") override val esPortHTTP = Props.get("es.warehouse.port.http","9200") From 17975dabd1a2821f2b0f288bb4a48049558b79d8 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 17 Jun 2016 04:54:41 -0700 Subject: [PATCH 541/702] renamed addEntitlements -> addEntitlement Added some ResourceDoc text . ENTITLEMENT -> ENTITLEMENT_ID --- .../scala/code/api/v2_0_0/APIMethods200.scala | 16 +++++++++------- src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index dbe1a791b..9138e8336 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1623,13 +1623,15 @@ trait APIMethods200 { } resourceDocs += ResourceDoc( - addEntitlements, + addEntitlement, apiVersion, - "addEntitlements", + "addEntitlement", "POST", "/users/USER_ID/entitlements", - "Add role to specific user.", - """Grants the Role to user. Creates an entitlement. + "Add Entitlement to a user.", + """Create Entitlement. Grant Role to User. + | + |Entitlements are used to grant system or bank level roles to users. (For account level privileges, see Views) | |Authentication is required and the user needs to be a Super Admin. Super Admins are listed in the Props file.""", Extraction.decompose(CreateEntitlementJSON("obp-bank-x-gh", "CanQueryOtherUser")), @@ -1640,7 +1642,7 @@ trait APIMethods200 { false, List()) - lazy val addEntitlements : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + lazy val addEntitlement : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a list of views case "users" :: userId :: "entitlements" :: Nil JsonPost json -> _ => { user => @@ -1702,8 +1704,8 @@ trait APIMethods200 { apiVersion, "deleteEntitlement", "DELETE", - "/users/USER_ID/entitlement/ENTITLEMENT", - "Delete Entitlement specified by ENTITLEMENT for an user specified by USER_ID", + "/users/USER_ID/entitlement/ENTITLEMENT_ID", + "Delete Entitlement specified by ENTITLEMENT_ID for an user specified by USER_ID", """ | |Authentication is required and the user needs to be a Super Admin. diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 69b777b24..e6359ae73 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -171,7 +171,7 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.createCustomer, Implementations2_0_0.getCurrentUser, Implementations2_0_0.createUserCustomerLinks, - Implementations2_0_0.addEntitlements, + Implementations2_0_0.addEntitlement, Implementations2_0_0.getEntitlements, Implementations2_0_0.deleteEntitlement, Implementations2_0_0.getAllEntitlements, From f258a0350b0fff22e976e5fa30a464b7d13c9a42 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Sat, 18 Jun 2016 01:38:40 +0200 Subject: [PATCH 542/702] Allow non account owner to create any Transation Request at a BANK_ID if they have canCreateAnyTransactionRequest Entitlement #52 --- src/main/scala/code/api/util/ApiRole.scala | 2 + .../scala/code/api/v2_0_0/APIMethods200.scala | 1 + .../scala/code/bankconnectors/Connector.scala | 7 +- .../api/LocalMappedConnectorTestSetup.scala | 12 + .../api/v2_0_0/TransactionRequestsTest.scala | 255 +++++++++++++++++- 5 files changed, 274 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala index 8f7b25cbe..bbbe4bd42 100644 --- a/src/main/scala/code/api/util/ApiRole.scala +++ b/src/main/scala/code/api/util/ApiRole.scala @@ -12,6 +12,7 @@ object ApiRole { case object CanCreateCustomer extends ApiRole case object CanCreateAccount extends ApiRole case object IsHackathonDeveloper extends ApiRole + case object CanCreateAnyTransactionRequest extends ApiRole def valueOf(value: String): ApiRole = value match { case "CanSearchAllTransactions" => CanSearchAllTransactions @@ -22,6 +23,7 @@ object ApiRole { case "CanCreateCustomer" => CanCreateCustomer case "CanCreateAccount" => CanCreateAccount case "IsHackathonDeveloper" => IsHackathonDeveloper + case "CanCreateAnyTransactionRequest" => CanCreateAnyTransactionRequest case _ => throw new IllegalArgumentException() } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index dbe1a791b..e63c367bf 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1145,6 +1145,7 @@ trait APIMethods200 { transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)} fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + isOwnerOrHasEntitlement <- booleanToBox(u.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, u.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toBankId <- tryo(BankId(transBodyJson.to.bank_id)) toAccountId <- tryo(AccountId(transBodyJson.to.account_id)) toAccount <- tryo{BankAccount(toBankId, toAccountId).get} ?~! {ErrorMessages.CounterpartyNotFound} diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index f2c89d914..4d1445347 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -1,5 +1,8 @@ package code.bankconnectors +import code.api.util.APIUtil._ +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages import code.management.ImporterAPI.ImporterTransaction import code.tesobe.CashTransaction import code.transactionrequests.TransactionRequests @@ -146,7 +149,7 @@ trait Connector { for { fromAccount <- getBankAccountType(fromAccountUID.bankId, fromAccountUID.accountId) ?~ s"account ${fromAccountUID.accountId} not found at bank ${fromAccountUID.bankId}" - isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") + isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccountUID.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true, ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toAccount <- getBankAccountType(toAccountUID.bankId, toAccountUID.accountId) ?~ s"account ${toAccountUID.accountId} not found at bank ${toAccountUID.bankId}" //sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { @@ -257,7 +260,7 @@ trait Connector { var result = for { fromAccountType <- getBankAccountType(fromAccount.bankId, fromAccount.accountId) ?~ s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" - isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") + isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toAccountType <- getBankAccountType(toAccount.bankId, toAccount.accountId) ?~ s"account ${toAccount.accountId} not found at bank ${toAccount.bankId}" rawAmt <- tryo { BigDecimal(body.value.amount) } ?~! s"amount ${body.value.amount} not convertible to number" diff --git a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala index ad950b3cc..53d154b35 100644 --- a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala @@ -5,8 +5,10 @@ import java.util.Date import bootstrap.liftweb.ToSchemify import code.model._ import code.model.dataAccess._ +import net.liftweb.common.Box import net.liftweb.mapper.MetaMapper import net.liftweb.util.Helpers._ +import code.entitlement.{MappedEntitlement, Entitlement} import scala.util.Random @@ -52,6 +54,16 @@ trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermis .accountLabel(randomString(4)).saveMe } + def addEntitlement(bankId: String, userId: String, roleName: String): Box[Entitlement] = { + // Return a Box so we can handle errors later. + val addEntitlement = MappedEntitlement.create + .mBankId(bankId) + .mUserId(userId) + .mRoleName(roleName) + .saveMe() + Some(addEntitlement) + } + override protected def createTransaction(account: BankAccount, startDate: Date, finishDate: Date) = { //ugly val mappedBankAccount = account.asInstanceOf[MappedBankAccount] diff --git a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala index 40033ace3..f9438dea6 100644 --- a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala @@ -1,6 +1,7 @@ package code.api.v2_0_0 -import code.api.{DefaultUsers, ServerSetupWithTestData} +import code.api.util.ErrorMessages +import code.api.{ErrorMessage, DefaultUsers, ServerSetupWithTestData} import code.api.util.APIUtil.OAuth._ import code.api.v1_2_1.AmountOfMoneyJSON import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJSON} @@ -11,6 +12,7 @@ import net.liftweb.json.JsonAST.JString import net.liftweb.json.Serialization.write import net.liftweb.util.Props import org.scalatest.Tag +import code.api.util.ApiRole._ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers with V200ServerSetup { @@ -27,6 +29,154 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers }) } + // No challenge, No FX (same currencies) + if (Props.getBool("transactionRequests_enabled", false) == false) { + ignore("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) {} + } else { + scenario("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at BANK_ID", TransactionRequest) { + val testBank = createBank("transactions-test-bank") + val bankId = testBank.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + + addEntitlement(bankId.value, obpuser3.userId, CanCreateAnyTransactionRequest.toString) + Then("We add entitlement to user3") + val hasEntitlement = code.api.util.APIUtil.hasEntitlement(bankId.value, obpuser3.userId, CanCreateAnyTransactionRequest) + hasEntitlement should equal(true) + + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } + + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } + + val fromAccount = getFromAccount + val toAccount = getToAccount + + val totalTransactionsBefore = transactionCount(fromAccount, toAccount) + + val beforeFromBalance = fromAccount.balance + val beforeToBalance = toAccount.balance + + //Create a transaction (request) + //1. get possible challenge types for from account + //2. create transaction request to to-account with one of the possible challenges + //3. answer challenge + //4. have a new transaction + + val transactionRequestId = TransactionRequestId("__trans1") + val toAccountJson = TransactionRequestAccountJSON(toAccount.bankId.value, toAccount.accountId.value) + + val amt = BigDecimal("12.50") + val bodyValue = AmountOfMoneyJSON("EUR", amt.toString()) + val transactionRequestBody = TransactionRequestBodyJSON(toAccountJson, bodyValue, "Test Transaction Request description") + + //call createTransactionRequest + var request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@(user3) + var response = makePostRequest(request, write(transactionRequestBody)) + Then("we should get a 201 created code") + response.code should equal(201) + + println(response.body) + + //created a transaction request, check some return values. As type is SANDBOX_TAN and value is < 1000, we expect no challenge + val transRequestId: String = (response.body \ "id") match { + case JString(i) => i + case _ => "" + } + Then("We should have some new transaction id") + transRequestId should not equal ("") + + val responseBody = response.body + + + val status: String = (response.body \ "status") match { + case JString(i) => i + case _ => "" + } + status should equal (code.transactionrequests.TransactionRequests.STATUS_COMPLETED) + + // Challenge should be null (none required) + var challenge = (response.body \ "challenge").children + challenge.size should equal(0) + + var transaction_ids = (response.body \ "transaction_ids") match { + case JString(i) => i + case _ => "" + } + //If user does not have access to owner or other view - they won’t be able to view transaction. Hence they can’t see the transaction_id + transaction_ids should not equal("") + + //call getTransactionRequests, check that we really created a transaction request + request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-requests").GET <@(user1) + response = makeGetRequest(request) + + Then("we should get a 200 ok code") + response.code should equal(200) + val transactionRequests = response.body.children + transactionRequests.size should not equal(0) + + + val tr2Body = response.body + + //check transaction_ids again + transaction_ids = (response.body \ "transaction_requests_with_charges" \ "transaction_ids") match { + case JString(i) => i + case _ => "" + } + transaction_ids should not equal("") + + //make sure that we also get no challenges back from this url (after getting from db) + challenge = (response.body \ "challenge").children + challenge.size should equal(0) + + //check that we created a new transaction (since no challenge) + request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transactions").GET <@(user1) + response = makeGetRequest(request) + + Then("we should get a 200 ok code") + response.code should equal(200) + val transactions = response.body.children + + transactions.size should equal(1) + + //check that the description has been set + println(response.body) + /*val description = (((response.body \ "transactions")(0) \ "details") \ "description") match { + case JString(i) => i + case _ => "" + } + description should not equal ("")*/ + + //check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least) + //(do it here even though the payments test does test makePayment already) + val rate = fx.exchangeRate (fromAccount.currency, toAccount.currency) + val convertedAmount = fx.convert(amt, rate) + val fromAccountBalance = getFromAccount.balance + And("the from account should have a balance smaller by the amount specified to pay") + fromAccountBalance should equal((beforeFromBalance - convertedAmount)) + + /* + And("the newest transaction for the account receiving the payment should have the proper amount") + newestToAccountTransaction.details.value.amount should equal(amt.toString) + */ + + And("the account receiving the payment should have a new balance plus the amount paid") + val toAccountBalance = getToAccount.balance + toAccountBalance should equal(beforeToBalance + convertedAmount) + + And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") + transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) + } + } + // No challenge, No FX (same currencies) if (Props.getBool("transactionRequests_enabled", false) == false) { @@ -165,6 +315,109 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) } + + + scenario("we create a transaction request with a user without owner view access", TransactionRequest) { + val testBank = createBank("transactions-test-bank") + val bankId = testBank.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } + + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } + + val fromAccount = getFromAccount + val toAccount = getToAccount + + val toAccountJson = TransactionRequestAccountJSON(toAccount.bankId.value, toAccount.accountId.value) + + val amt = BigDecimal("12.50") + val bodyValue = AmountOfMoneyJSON("EUR", amt.toString()) + val transactionRequestBody = TransactionRequestBodyJSON(toAccountJson, bodyValue, "Test Transaction Request description") + + //call createTransactionRequest with a user without owner view access + val request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@(user2) + val response = makePostRequest(request, write(transactionRequestBody)) + Then("we should get a 400 created code") + response.code should equal(400) + + //created a transaction request, check some return values. As type is SANDBOX_TAN and value is < 1000, we expect no challenge + val error: String = (response.body \ "error") match { + case JString(i) => i + case _ => "" + } + Then("We should have the error: " + ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + error should equal (ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + + } + + + + + + scenario("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at a different BANK_ID", TransactionRequest) { + val testBank = createBank("transactions-test-bank") + val testBank2 = createBank("transactions-test-bank2") + val bankId = testBank.bankId + val bankId2 = testBank2.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + addEntitlement(bankId2.value, obpuser3.userId, CanCreateAnyTransactionRequest.toString) + + Then("We add entitlement to user3") + val hasEntitlement = code.api.util.APIUtil.hasEntitlement(bankId2.value, obpuser3.userId, CanCreateAnyTransactionRequest) + hasEntitlement should equal(true) + + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } + + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } + + val fromAccount = getFromAccount + val toAccount = getToAccount + + val totalTransactionsBefore = transactionCount(fromAccount, toAccount) + + val beforeFromBalance = fromAccount.balance + val beforeToBalance = toAccount.balance + + val transactionRequestId = TransactionRequestId("__trans2") + val toAccountJson = TransactionRequestAccountJSON(toAccount.bankId.value, toAccount.accountId.value) + + val amt = BigDecimal("12.50") + val bodyValue = AmountOfMoneyJSON("EUR", amt.toString()) + val transactionRequestBody = TransactionRequestBodyJSON(toAccountJson, bodyValue, "Test Transaction Request description") + + //call createTransactionRequest + val request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@ (user3) + val response = makePostRequest(request, write(transactionRequestBody)) + Then("we should get a 400 created code") + response.code should equal(400) + + //created a transaction request, check some return values. As type is SANDBOX_TAN and value is < 1000, we expect no challenge + val error: String = (response.body \ "error") match { + case JString(i) => i + case _ => "" + } + Then("We should have the error: " + ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + error should equal (ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + + + } } // No challenge, with FX From 2664f5054a5b7f227aa346552200310fcd1db90a Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Sat, 18 Jun 2016 09:16:36 +0200 Subject: [PATCH 543/702] Allow non account owner to create any Transation Request at a BANK_ID if they have canCreateAnyTransactionRequest Entitlement #52 --- .../code/api/v2_0_0/TransactionRequestsTest.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala index f9438dea6..7a50a3d52 100644 --- a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala @@ -31,7 +31,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers // No challenge, No FX (same currencies) if (Props.getBool("transactionRequests_enabled", false) == false) { - ignore("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) {} + ignore("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at BANK_ID", TransactionRequest) {} } else { scenario("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at BANK_ID", TransactionRequest) { val testBank = createBank("transactions-test-bank") @@ -315,8 +315,11 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) } + } - + if (Props.getBool("transactionRequests_enabled", false) == false) { + ignore("we create a transaction request with a user without owner view access", TransactionRequest) {} + } else { scenario("we create a transaction request with a user without owner view access", TransactionRequest) { val testBank = createBank("transactions-test-bank") val bankId = testBank.bankId @@ -359,10 +362,11 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers } + } - - - + if (Props.getBool("transactionRequests_enabled", false) == false) { + ignore("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at a different BANK_ID", TransactionRequest) {} + } else { scenario("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at a different BANK_ID", TransactionRequest) { val testBank = createBank("transactions-test-bank") val testBank2 = createBank("transactions-test-bank2") From 024e14793d1eaf442c6d371bbde313311d0d0324 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 20 Jun 2016 11:08:37 +0200 Subject: [PATCH 544/702] Added getUser endpoint that returns user by email address --- src/main/scala/code/api/util/ApiRole.scala | 2 + .../scala/code/api/v2_0_0/APIMethods200.scala | 43 +++++++++++++++++++ .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 8 +--- .../scala/code/model/dataAccess/OBPUser.scala | 4 ++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/main/scala/code/api/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala index 8f7b25cbe..620908771 100644 --- a/src/main/scala/code/api/util/ApiRole.scala +++ b/src/main/scala/code/api/util/ApiRole.scala @@ -11,6 +11,7 @@ object ApiRole { case object CanSearchMetrics extends ApiRole case object CanCreateCustomer extends ApiRole case object CanCreateAccount extends ApiRole + case object CanGetAnyUser extends ApiRole case object IsHackathonDeveloper extends ApiRole def valueOf(value: String): ApiRole = value match { @@ -21,6 +22,7 @@ object ApiRole { case "CanSearchMetrics" => CanSearchMetrics case "CanCreateCustomer" => CanCreateCustomer case "CanCreateAccount" => CanCreateAccount + case "CanGetAnyUser" => CanGetAnyUser case "IsHackathonDeveloper" => IsHackathonDeveloper case _ => throw new IllegalArgumentException() } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 9138e8336..72707d117 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -13,6 +13,7 @@ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121 import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, CustomerFaceImageJson, TransactionRequestAccountJSON} import code.entitlement.Entitlement import code.search.{elasticsearchMetrics, elasticsearchWarehouse} +import net.liftweb.http.CurrentReq //import code.api.v2_0_0.{CreateCustomerJson} import code.model.dataAccess.OBPUser @@ -1583,6 +1584,48 @@ trait APIMethods200 { } } + + resourceDocs += ResourceDoc( + getUser, + apiVersion, + "getUser", + "GET", + "/users/USER_EMAIL", + "Get User by Email Address", + """Get the user by email address + | + |Login is required. + |CanGetAnyUser entitlement is required, + | + """.stripMargin, + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + true, + true, + true, + List(apiTagUser)) + + + lazy val getUser: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "users" :: userEmail :: Nil JsonGet _ => { + user => + for { + l <- user ?~ ErrorMessages.UserNotLoggedIn + b <- Bank.all.headOption //TODO: This is a temp workaround + canGetAnyUser <- booleanToBox(hasEntitlement(b.bankId.value, l.userId, ApiRole.CanGetAnyUser), "CanGetAnyUser entitlement required") + // Workaround to get userEmail address directly from URI without needing to URL-encode it + u <- OBPUser.getApiUserByEmail(CurrentReq.value.uri.split("/").last) + } + yield { + // Format the data as V2.0.0 json + val json = JSONFactory200.createUserJSON(u) + successJsonResponse(Extraction.decompose(json)) + } + } + } + + resourceDocs += ResourceDoc( createUserCustomerLinks, apiVersion, diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index e6359ae73..ad53eb687 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -170,6 +170,7 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.getMeeting, Implementations2_0_0.createCustomer, Implementations2_0_0.getCurrentUser, + Implementations2_0_0.getUser, Implementations2_0_0.createUserCustomerLinks, Implementations2_0_0.addEntitlement, Implementations2_0_0.getEntitlements, @@ -179,13 +180,6 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.elasticSearchMetrics ) -// if (Props.getBool("allow_elasticsearch", false)) { -// if (Props.getBool("allow_elasticsearch_warehouse", false)) -// routes = Implementations2_0_0.elasticSearchWarehouse :: routes -// if (Props.getBool("allow_elasticsearch_metrics", false)) -// routes = Implementations2_0_0.elasticSearchMetrics :: routes -// } - routes.foreach(route => { oauthServe(apiPrefix{route}) }) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 76006b9d4..88d068b1e 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -92,6 +92,10 @@ class OBPUser extends MegaProtoUser[OBPUser] with Logger { .providerId(email) } + def getApiUserByEmail(userEmail: String) : Box[APIUser] = { + APIUser.find(By(APIUser.email, userEmail)) + } + override def save(): Boolean = { if(! (user defined_?)){ info("user reference is null. We will create an API User") From b82e8a01cb2356bf706f0b7fcecf7db472cc990d Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 20 Jun 2016 11:17:56 +0200 Subject: [PATCH 545/702] Added and tested elasticsearch parameers scroll and scroll_id --- src/main/scala/code/search/search.scala | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala index effd36585..028b05fc0 100644 --- a/src/main/scala/code/search/search.scala +++ b/src/main/scala/code/search/search.scala @@ -62,10 +62,10 @@ class elasticsearch extends Loggable { } private def constructQuery(userId: String, params: Map[String, String]): Req = { + var esScroll = "" val esType = params.getOrElse("esType", "") val q = params.getOrElse("q", "") val source = params.getOrElse("source","") - //val filteredParams = params -- Set("esIndex", "esType") //val jsonQuery = Json.encode(filteredParams) //TODO: Workaround - HTTP and TCP ports differ. Should there be props entry for both? val httpHost = ("http://" + esHost + ":" + esPortHTTP) @@ -77,6 +77,9 @@ class elasticsearch extends Loggable { val sort = params.getOrElse("sort", "") val from = params.getOrElse("from", "") val df = params.getOrElse("df", "") + val scroll = params.getOrElse("scroll", "") + val scroll_id = params.getOrElse("scroll_id", "") + val search_type = params.getOrElse("search_type", "") if (size != "") parameters = parameters ++ Seq(("size", size)) if (sort != "") @@ -85,11 +88,20 @@ class elasticsearch extends Loggable { parameters = parameters ++ Seq(("from", from)) if (df != "") parameters = parameters ++ Seq(("df", df)) + if (scroll != "") + parameters = parameters ++ Seq(("scroll", scroll)) + if (search_type != "") + parameters = parameters ++ Seq(("search_type", search_type)) + // scroll needs specific URL + if (scroll_id != "" && scroll != "") { + esScroll = "/scroll" + parameters = Seq(("scroll", scroll)) ++ Seq(("scroll_id", scroll_id)) + } } else if (q == "" && source != "") { parameters = Seq(("source", source)) } - val esUrl = Helpers.appendParams( s"${httpHost}/${esIndex}/${esType}${if (esType.nonEmpty) "/" else ""}_search", parameters ) + val esUrl = Helpers.appendParams( s"${httpHost}/${esIndex}/${esType}${if (esType.nonEmpty) "/" else ""}_search${esScroll}", parameters ) println("[ES.URL]===> " + esUrl) // Use this incase we cant log to elastic search From eaa85195d06c89b0cd18bfb5680c3d6730acea9d Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 20 Jun 2016 11:44:56 +0200 Subject: [PATCH 546/702] Fixed broken refactoring of getBankAccountType -> getSingleBankAccount --- .../scala/code/bankconnectors/Connector.scala | 36 +++++++++---------- .../bankconnectors/KafkaMappedConnector.scala | 20 +++++------ .../code/bankconnectors/LocalConnector.scala | 6 ++-- .../bankconnectors/LocalMappedConnector.scala | 10 +++--- .../code/api/v1_3_0/PhysicalCardsTest.scala | 2 +- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 4d1445347..5e1aefbf5 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -1,25 +1,21 @@ package code.bankconnectors +import java.util.Date + import code.api.util.APIUtil._ import code.api.util.ApiRole._ import code.api.util.ErrorMessages +import code.fx.fx import code.management.ImporterAPI.ImporterTransaction +import code.model.{OtherBankAccount, Transaction, User, _} import code.tesobe.CashTransaction import code.transactionrequests.TransactionRequests -import code.transactionrequests.TransactionRequests.{TransactionRequestCharge, TransactionRequest, TransactionRequestBody, TransactionRequestChallenge} +import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} import code.util.Helper._ import net.liftweb.common.{Box, Empty, Failure, Full} -import code.model._ import net.liftweb.util.Helpers._ import net.liftweb.util.{Props, SimpleInjector} -import code.model.User -import code.model.OtherBankAccount -import code.model.Transaction -import java.util.Date -import code.fx.fx - -import scala.math.BigDecimal.RoundingMode import scala.math.BigInt import scala.util.Random @@ -82,7 +78,7 @@ trait Connector { def getBanks : List[Bank] def getBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] = - getBankAccountType(bankId, accountId) + getSingleBankAccount(bankId, accountId) def getBankAccounts(accounts: List[(BankId, AccountId)]) : List[BankAccount] = { for ( acc <- accounts ) yield { @@ -90,7 +86,7 @@ trait Connector { } } - protected def getBankAccountType(bankId : BankId, accountId : AccountId) : Box[AccountType] + protected def getSingleBankAccount(bankId : BankId, accountId : AccountId) : Box[AccountType] def getOtherBankAccount(bankId: BankId, accountID : AccountId, otherAccountID : String) : Box[OtherBankAccount] @@ -103,7 +99,7 @@ trait Connector { def getPhysicalCards(user : User) : Set[PhysicalCard] def getPhysicalCardsForBank(bankId: BankId, user : User) : Set[PhysicalCard] - + //gets the users who are the legal owners/holders of the account def getAccountHolders(bankId: BankId, accountID: AccountId) : Set[User] @@ -121,10 +117,10 @@ trait Connector { def makePayment(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID, amt : BigDecimal, description : String) : Box[TransactionId] = { for{ - fromAccount <- getBankAccountType(fromAccountUID.bankId, fromAccountUID.accountId) ?~ + fromAccount <- getSingleBankAccount(fromAccountUID.bankId, fromAccountUID.accountId) ?~ s"account ${fromAccountUID.accountId} not found at bank ${fromAccountUID.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") - toAccount <- getBankAccountType(toAccountUID.bankId, toAccountUID.accountId) ?~ + toAccount <- getSingleBankAccount(toAccountUID.bankId, toAccountUID.accountId) ?~ s"account ${toAccountUID.accountId} not found at bank ${toAccountUID.bankId}" sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { s"Cannot send payment to account with different currency (From ${fromAccount.currency} to ${toAccount.currency}" @@ -147,10 +143,10 @@ trait Connector { def makePaymentv200(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID, amt : BigDecimal, description : String) : Box[TransactionId] = { for { - fromAccount <- getBankAccountType(fromAccountUID.bankId, fromAccountUID.accountId) ?~ + fromAccount <- getSingleBankAccount(fromAccountUID.bankId, fromAccountUID.accountId) ?~ s"account ${fromAccountUID.accountId} not found at bank ${fromAccountUID.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccountUID.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true, ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) - toAccount <- getBankAccountType(toAccountUID.bankId, toAccountUID.accountId) ?~ + toAccount <- getSingleBankAccount(toAccountUID.bankId, toAccountUID.accountId) ?~ s"account ${toAccountUID.accountId} not found at bank ${toAccountUID.bankId}" //sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { // s"Cannot send payment to account with different currency (From ${fromAccount.currency} to ${toAccount.currency}" @@ -198,10 +194,10 @@ trait Connector { //create a new transaction request var result = for { - fromAccountType <- getBankAccountType(fromAccount.bankId, fromAccount.accountId) ?~ + fromAccountType <- getSingleBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") - toAccountType <- getBankAccountType(toAccount.bankId, toAccount.accountId) ?~ + toAccountType <- getSingleBankAccount(toAccount.bankId, toAccount.accountId) ?~ s"account ${toAccount.accountId} not found at bank ${toAccount.bankId}" rawAmt <- tryo { BigDecimal(body.value.amount) } ?~! s"amount ${body.value.amount} not convertible to number" sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { @@ -258,10 +254,10 @@ trait Connector { // Always create a new Transaction Request var result = for { - fromAccountType <- getBankAccountType(fromAccount.bankId, fromAccount.accountId) ?~ + fromAccountType <- getSingleBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) - toAccountType <- getBankAccountType(toAccount.bankId, toAccount.accountId) ?~ + toAccountType <- getSingleBankAccount(toAccount.bankId, toAccount.accountId) ?~ s"account ${toAccount.accountId} not found at bank ${toAccount.bankId}" rawAmt <- tryo { BigDecimal(body.value.amount) } ?~! s"amount ${body.value.amount} not convertible to number" // isValidTransactionRequestType is checked at API layer. Maybe here too. diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index e7357930c..4df452b40 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -260,7 +260,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable return Full(res) } - override def getBankAccountType(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { + override def getSingleBankAccount(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId @@ -602,7 +602,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable ) //delete account - val account = getBankAccountType(bankId, accountID) + val account = getSingleBankAccount(bankId, accountID) val accountDeleted = account match { case acc => true //acc.delete_! //TODO @@ -638,7 +638,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable private def createAccountIfNotExisting(bankId: BankId, accountID: AccountId, accountNumber: String, currency: String, balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = { - getBankAccountType(bankId, accountID) match { + getSingleBankAccount(bankId, accountID) match { case Full(a) => logger.info(s"account with id $accountID at bank with id $bankId already exists. No need to create a new one.") a @@ -666,7 +666,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountID override def getAccountByUUID(uuid: String): Box[AccountType] = { - getBankAccountType(null, AccountId(uuid)) + getSingleBankAccount(null, AccountId(uuid)) } //cash api requires a call to add a new transaction and update the account balance @@ -729,7 +729,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable //this will be Full(true) if everything went well val result = for { - acc <- getBankAccountType(bankId, accountID) + acc <- getSingleBankAccount(bankId, accountID) bank <- getBank(bankId) } yield { //acc.balance = newBalance @@ -821,7 +821,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable bankId <- getBankByNationalIdentifier(bankNationalIdentifier).map(_.bankId) account <- getAccountByNumber(bankId, accountNumber) } yield { - val acc = getBankAccountType(bankId, account.accountId) + val acc = getSingleBankAccount(bankId, account.accountId) acc match { case a => true //a.lastUpdate = updateDate //TODO case _ => logger.warn("can't set bank account.lastUpdated because the account was not found"); false @@ -838,7 +838,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable override def updateAccountLabel(bankId: BankId, accountID: AccountId, label: String): Boolean = { //this will be Full(true) if everything went well val result = for { - acc <- getBankAccountType(bankId, accountID) + acc <- getSingleBankAccount(bankId, accountID) bank <- getBank(bankId) } yield { //acc.label = label @@ -886,8 +886,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable if (r.details.completed != null) // && r.details.completed.matches("^[0-9]{8}$")) dateCompleted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.completed) - val c = getBankAccountType(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).orNull - val o = getBankAccountType(BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull + val c = getSingleBankAccount(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).orNull + val o = getSingleBankAccount(BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) val dummyOtherBankAccount = createOtherBankAccount(c, o, None) //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object @@ -898,7 +898,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable new Transaction( r.id, // uuid:String TransactionId(r.id), // id:TransactionId - getBankAccountType( BankId(r.this_account.bank), + getSingleBankAccount( BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull, // thisAccount:BankAccount otherAccount, // otherAccount:OtherBankAccount r.details.`type`, // transactionType:String diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index dbf5a08ad..492dd24cc 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -36,7 +36,7 @@ private object LocalConnector extends Connector with Loggable { override def getBanks : List[Bank] = HostedBank.findAll - override def getBankAccountType(bankId : BankId, accountId : AccountId) : Box[Account] = { + override def getSingleBankAccount(bankId : BankId, accountId : AccountId) : Box[Account] = { for{ bank <- getHostedBank(bankId) account <- bank.getAccount(accountId) @@ -587,7 +587,7 @@ private object LocalConnector extends Connector with Loggable { //used by the transaction import api override def updateAccountBalance(bankId: BankId, accountId: AccountId, newBalance: BigDecimal): Boolean = { - getBankAccountType(bankId, accountId) match { + getSingleBankAccount(bankId, accountId) match { case Full(acc) => acc.accountBalance(newBalance).saveTheRecord().isDefined true @@ -607,7 +607,7 @@ private object LocalConnector extends Connector with Loggable { } override def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean = { - getBankAccountType(bankId, accountId) match { + getSingleBankAccount(bankId, accountId) match { case Full(acc) => acc.accountLabel(label).saveTheRecord().isDefined true diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 4a8fd58c4..a27809e36 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -90,7 +90,7 @@ object LocalMappedConnector extends Connector with Loggable { for { bank <- getMappedBank(bankId) - account <- getBankAccountType(bankId, accountId) + account <- getSingleBankAccount(bankId, accountId) } { Future{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) @@ -106,7 +106,7 @@ object LocalMappedConnector extends Connector with Loggable { } // Question: Why is this called getBankAccountType? Why not getBankAccount? TODO rename - override def getBankAccountType(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = { + override def getSingleBankAccount(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = { MappedBankAccount.find( By(MappedBankAccount.bank, bankId.value), By(MappedBankAccount.theAccountId, accountId.value)) @@ -428,7 +428,7 @@ object LocalMappedConnector extends Connector with Loggable { private def createAccountIfNotExisting(bankId: BankId, accountId: AccountId, accountNumber: String, currency: String, balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = { - getBankAccountType(bankId, accountId) match { + getSingleBankAccount(bankId, accountId) match { case Full(a) => logger.info(s"account with id $accountId at bank with id $bankId already exists. No need to create a new one.") a @@ -517,7 +517,7 @@ object LocalMappedConnector extends Connector with Loggable { //this will be Full(true) if everything went well val result = for { - acc <- getBankAccountType(bankId, accountId) + acc <- getSingleBankAccount(bankId, accountId) bank <- getMappedBank(bankId) } yield { acc.accountBalance(Helper.convertToSmallestCurrencyUnits(newBalance, acc.currency)).save @@ -634,7 +634,7 @@ object LocalMappedConnector extends Connector with Loggable { override def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean = { //this will be Full(true) if everything went well val result = for { - acc <- getBankAccountType(bankId, accountId) + acc <- getSingleBankAccount(bankId, accountId) bank <- getMappedBank(bankId) } yield { acc.accountLabel(label).save diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index 532da4c25..03373e802 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -55,7 +55,7 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { //these methods aren't required by our test override def getBank(bankId : BankId) : Box[Bank] = Empty override def getBanks : List[Bank] = Nil - override def getBankAccountType(bankId : BankId, accountId : AccountId) : Box[BankAccount] = Empty + override def getSingleBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] = Empty override def getOtherBankAccount(bankId: BankId, accountID : AccountId, otherAccountID : String) : Box[OtherBankAccount] = Empty override def getOtherBankAccounts(bankId: BankId, accountID : AccountId): List[OtherBankAccount] = From bfbfb2d27e4e9738a5dad40c92ba1f477e33c631 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 20 Jun 2016 15:50:51 +0200 Subject: [PATCH 547/702] This close #49 - Write tests for user customer links end point --- .../MappedUserCustomerLinkProviderTest.scala | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/test/scala/code/usercustomerlinks/MappedUserCustomerLinkProviderTest.scala diff --git a/src/test/scala/code/usercustomerlinks/MappedUserCustomerLinkProviderTest.scala b/src/test/scala/code/usercustomerlinks/MappedUserCustomerLinkProviderTest.scala new file mode 100644 index 000000000..5ac47cf52 --- /dev/null +++ b/src/test/scala/code/usercustomerlinks/MappedUserCustomerLinkProviderTest.scala @@ -0,0 +1,93 @@ +package code.usercustomerlinks + +import java.util.Date + +import code.api.ServerSetup +import net.liftweb.mapper.By + +class MappedUserCustomerLinkProviderTest extends ServerSetup { + + val customerId1 = "5ada3287-c045-4683-a28f-492c19787460" + val userId1 = "0ea67c89-d51b-4b24-9fd8-3ee535c2b473" + val customerId2 = "551d2aaa-e5af-416e-ba82-25154d65a9cf" + val userId2 = "3febd61e-3551-460d-a2a0-128a8a177d19" + + def userCustomerLink(userId: String, customerId: String) = MappedUserCustomerLink.create + .mCustomerId(customerId) + .mUserId(userId) + .mDateInserted(new Date(12340000)) + .mIsActive(true).saveMe + + private def delete() { + MappedUserCustomerLink.bulkDelete_!!() + } + + override def beforeAll() = { + super.beforeAll() + delete() + } + + override def afterEach() = { + super.afterEach() + delete() + } + + + feature("Getting user to customer link data") { + + scenario("We try to get UserCustomerLink") { + Given("There is no user to customer link at all but we try to get it") + MappedUserCustomerLink.findAll().size should equal(0) + + When("We try to get it all") + val found = MappedUserCustomerLink.getUserCustomerLinks.openOr(List()) + + Then("We don't") + found.size should equal(0) + } + + + scenario("A UserCustomerLink exists for user and we try to get it") { + val userCustomerLink1 = userCustomerLink(userId1, customerId1) + Given("Create a user to customer link") + MappedUserCustomerLink.find( + By(MappedUserCustomerLink.mUserId, userId1), + By(MappedUserCustomerLink.mCustomerId, customerId1) + ).isDefined should equal(true) + + When("We try to get it by user and customer") + val foundOpt = MappedUserCustomerLink.getUserCustomerLink(userId1, customerId1) + + Then("We do") + foundOpt.isDefined should equal(true) + + And("It is the right thing") + val foundThing = foundOpt.get + foundThing should equal(userCustomerLink1) + + And("Primary id should be UUID") + foundThing.userCustomerLinkId.filter(_ != '-').size should equal(32) + } + + scenario("We try to get all UserCustomerLink rows"){ + val userCustomerLink1 = userCustomerLink(userId1, customerId1) + val userCustomerLink2 = userCustomerLink(userId2, customerId2) + + When("We try to get it all") + val found = MappedUserCustomerLink.getUserCustomerLinks.openOr(List()) + + Then("We don't") + found.size should equal(2) + + And("We try to get it by user1 and customer1") + val foundThing1 = found.filter(_.userId == userId1).filter(_.customerId == customerId1) + foundThing1 should equal(List(userCustomerLink1)) + + And("We try to get it by user2 and customer2") + val foundThing2 = found.filter(_.userId == userId2).filter(_.customerId == customerId2) + foundThing2 should equal(List(userCustomerLink2)) + + } + + } +} From f65936e738714ab7f716d5f1d751889908fa0d99 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 20 Jun 2016 11:03:05 -0700 Subject: [PATCH 548/702] Transaction Request docs closes #64 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 7890662cb..bab15cbe1 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1116,7 +1116,17 @@ trait APIMethods200 { | |The following static FX rates are available in sandbox mode: | - |${exchangeRates}""", + |${exchangeRates} + | + | + |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL + | + |The payee is set in the request body. Money goes into the BANK_ID and ACCOUNT_ID specified in the request body. + | + | + | + | + |""", Extraction.decompose(TransactionRequestBodyJSON ( TransactionRequestAccountJSON("BANK_ID", "ACCOUNT_ID"), AmountOfMoneyJSON121("EUR", "100.53"), From ee9e3d5cadd46718eed99d663e5fde1ae078c1b9 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 21 Jun 2016 09:36:27 +0200 Subject: [PATCH 549/702] This closes #57 - Wrong password throws 500 error when Direct Login --- src/main/scala/code/model/dataAccess/OBPUser.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 88d068b1e..a032b7b3e 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -289,14 +289,17 @@ import net.liftweb.util.Helpers._ user.id.toLong } else { - getExternalUser(username, password).get.id.toLong + Props.get("connector").openOrThrowException("no connector set") match { + case "kafka" => getUserFromKafka(username, password).get.id.toLong + case _ => 0 + } } } case _ => 0 } } - def getExternalUser(username: String, password: String):Box[OBPUser] = { + def getUserFromKafka(username: String, password: String):Box[OBPUser] = { KafkaMappedConnector.getUser(username, password) match { case Full(KafkaInboundUser(extEmail, extPassword, extDisplayName)) => { info("external user authenticated. login redir: " + loginRedirect.get) @@ -381,7 +384,7 @@ import net.liftweb.util.Helpers._ // If not found locally, try to authenticate user via Kafka, if enabled in props if (Props.get("connector").openOrThrowException("no connector set") == "kafka") { val preLoginState = capturePreLoginState() - val extUser = getExternalUser(S.param("username").orNull, S.param("password").orNull) + val extUser = getUserFromKafka(S.param("username").orNull, S.param("password").orNull) if (!extUser.isEmpty) { val u = APIUser.find(By(APIUser.email, extUser.getOrElse(null).email)).getOrElse(null) From 83632d6a29d49fb5a7634c77ac4787b7b38bfc18 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Tue, 21 Jun 2016 11:34:37 +0200 Subject: [PATCH 550/702] This closes #17 - Sort permissions when getting views --- .../code/api/v1_2_1/JSONFactory1.2.1.scala | 204 +++++++++--------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.scala b/src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.scala index c6583f154..63bb94953 100644 --- a/src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.scala +++ b/src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.scala @@ -75,65 +75,65 @@ class ViewJSON( val is_public: Boolean, val alias: String, val hide_metadata_if_alias_used: Boolean, - val can_see_transaction_this_bank_account: Boolean, - val can_see_transaction_other_bank_account: Boolean, - val can_see_transaction_metadata: Boolean, - val can_see_transaction_description: Boolean, - val can_see_transaction_amount: Boolean, - val can_see_transaction_type: Boolean, - val can_see_transaction_currency: Boolean, - val can_see_transaction_start_date: Boolean, - val can_see_transaction_finish_date: Boolean, - val can_see_transaction_balance: Boolean, - val can_see_comments: Boolean, - val can_see_owner_comment: Boolean, - val can_see_tags: Boolean, - val can_see_images: Boolean, - val can_see_bank_account_owners: Boolean, - val can_see_bank_account_type: Boolean, + val can_add_comment : Boolean, + val can_add_corporate_location : Boolean, + val can_add_image : Boolean, + val can_add_image_url: Boolean, + val can_add_more_info: Boolean, + val can_add_open_corporates_url : Boolean, + val can_add_physical_location : Boolean, + val can_add_private_alias : Boolean, + val can_add_public_alias : Boolean, + val can_add_tag : Boolean, + val can_add_url: Boolean, + val can_add_where_tag : Boolean, + val can_delete_comment: Boolean, + val can_delete_corporate_location : Boolean, + val can_delete_image : Boolean, + val can_delete_physical_location : Boolean, + val can_delete_tag : Boolean, + val can_delete_where_tag : Boolean, + val can_edit_owner_comment: Boolean, val can_see_bank_account_balance: Boolean, + val can_see_bank_account_bank_name: Boolean, val can_see_bank_account_currency: Boolean, + val can_see_bank_account_iban: Boolean, val can_see_bank_account_label: Boolean, val can_see_bank_account_national_identifier: Boolean, - val can_see_bank_account_swift_bic: Boolean, - val can_see_bank_account_iban: Boolean, val can_see_bank_account_number: Boolean, - val can_see_bank_account_bank_name: Boolean, - val can_see_other_account_national_identifier: Boolean, - val can_see_other_account_swift_bic: Boolean, - val can_see_other_account_iban: Boolean, - val can_see_other_account_bank_name: Boolean, - val can_see_other_account_number: Boolean, - val can_see_other_account_metadata: Boolean, - val can_see_other_account_kind: Boolean, - val can_see_more_info: Boolean, - val can_see_url: Boolean, - val can_see_image_url: Boolean, - val can_see_open_corporates_url: Boolean, + val can_see_bank_account_owners: Boolean, + val can_see_bank_account_swift_bic: Boolean, + val can_see_bank_account_type: Boolean, + val can_see_comments: Boolean, val can_see_corporate_location: Boolean, + val can_see_image_url: Boolean, + val can_see_images: Boolean, + val can_see_more_info: Boolean, + val can_see_open_corporates_url: Boolean, + val can_see_other_account_bank_name: Boolean, + val can_see_other_account_iban: Boolean, + val can_see_other_account_kind: Boolean, + val can_see_other_account_metadata: Boolean, + val can_see_other_account_national_identifier: Boolean, + val can_see_other_account_number: Boolean, + val can_see_other_account_swift_bic: Boolean, + val can_see_owner_comment: Boolean, val can_see_physical_location: Boolean, - val can_see_public_alias: Boolean, val can_see_private_alias: Boolean, - val can_add_more_info: Boolean, - val can_add_url: Boolean, - val can_add_image_url: Boolean, - val can_add_open_corporates_url : Boolean, - val can_add_corporate_location : Boolean, - val can_add_physical_location : Boolean, - val can_add_public_alias : Boolean, - val can_add_private_alias : Boolean, - val can_delete_corporate_location : Boolean, - val can_delete_physical_location : Boolean, - val can_edit_owner_comment: Boolean, - val can_add_comment : Boolean, - val can_delete_comment: Boolean, - val can_add_tag : Boolean, - val can_delete_tag : Boolean, - val can_add_image : Boolean, - val can_delete_image : Boolean, - val can_add_where_tag : Boolean, - val can_see_where_tag : Boolean, - val can_delete_where_tag : Boolean + val can_see_public_alias: Boolean, + val can_see_tags: Boolean, + val can_see_transaction_amount: Boolean, + val can_see_transaction_balance: Boolean, + val can_see_transaction_currency: Boolean, + val can_see_transaction_description: Boolean, + val can_see_transaction_finish_date: Boolean, + val can_see_transaction_metadata: Boolean, + val can_see_transaction_other_bank_account: Boolean, + val can_see_transaction_start_date: Boolean, + val can_see_transaction_this_bank_account: Boolean, + val can_see_transaction_type: Boolean, + val can_see_url: Boolean, + val can_see_where_tag : Boolean ) case class AccountsJSON( accounts : List[AccountJSON] @@ -367,65 +367,65 @@ object JSONFactory{ is_public = view.isPublic, alias = alias, hide_metadata_if_alias_used = view.hideOtherAccountMetadataIfAlias, - can_see_transaction_this_bank_account = view.canSeeTransactionThisBankAccount, - can_see_transaction_other_bank_account = view.canSeeTransactionOtherBankAccount, - can_see_transaction_metadata = view.canSeeTransactionMetadata, - can_see_transaction_description = view.canSeeTransactionDescription, - can_see_transaction_amount = view.canSeeTransactionAmount, - can_see_transaction_type = view.canSeeTransactionType, - can_see_transaction_currency = view.canSeeTransactionCurrency, - can_see_transaction_start_date = view.canSeeTransactionStartDate, - can_see_transaction_finish_date = view.canSeeTransactionFinishDate, - can_see_transaction_balance = view.canSeeTransactionBalance, - can_see_comments = view.canSeeComments, - can_see_owner_comment = view.canSeeOwnerComment, - can_see_tags = view.canSeeTags, - can_see_images = view.canSeeImages, - can_see_bank_account_owners = view.canSeeBankAccountOwners, - can_see_bank_account_type = view.canSeeBankAccountType, + can_add_comment = view.canAddComment, + can_add_corporate_location = view.canAddCorporateLocation, + can_add_image = view.canAddImage, + can_add_image_url = view.canAddImageURL, + can_add_more_info = view.canAddMoreInfo, + can_add_open_corporates_url = view.canAddOpenCorporatesUrl, + can_add_physical_location = view.canAddPhysicalLocation, + can_add_private_alias = view.canAddPrivateAlias, + can_add_public_alias = view.canAddPublicAlias, + can_add_tag = view.canAddTag, + can_add_url = view.canAddURL, + can_add_where_tag = view.canAddWhereTag, + can_delete_comment = view.canDeleteComment, + can_delete_corporate_location = view.canDeleteCorporateLocation, + can_delete_image = view.canDeleteImage, + can_delete_physical_location = view.canDeletePhysicalLocation, + can_delete_tag = view.canDeleteTag, + can_delete_where_tag = view.canDeleteWhereTag, + can_edit_owner_comment = view.canEditOwnerComment, can_see_bank_account_balance = view.canSeeBankAccountBalance, + can_see_bank_account_bank_name = view.canSeeBankAccountBankName, can_see_bank_account_currency = view.canSeeBankAccountCurrency, + can_see_bank_account_iban = view.canSeeBankAccountIban, can_see_bank_account_label = view.canSeeBankAccountLabel, can_see_bank_account_national_identifier = view.canSeeBankAccountNationalIdentifier, - can_see_bank_account_swift_bic = view.canSeeBankAccountSwift_bic, - can_see_bank_account_iban = view.canSeeBankAccountIban, can_see_bank_account_number = view.canSeeBankAccountNumber, - can_see_bank_account_bank_name = view.canSeeBankAccountBankName, - can_see_other_account_national_identifier = view.canSeeOtherAccountNationalIdentifier, - can_see_other_account_swift_bic = view.canSeeOtherAccountSWIFT_BIC, - can_see_other_account_iban = view.canSeeOtherAccountIBAN, - can_see_other_account_bank_name = view.canSeeOtherAccountBankName, - can_see_other_account_number = view.canSeeOtherAccountNumber, - can_see_other_account_metadata = view.canSeeOtherAccountMetadata, - can_see_other_account_kind = view.canSeeOtherAccountKind, - can_see_more_info = view.canSeeMoreInfo, - can_see_url = view.canSeeUrl, - can_see_image_url = view.canSeeImageUrl, - can_see_open_corporates_url = view.canSeeOpenCorporatesUrl, + can_see_bank_account_owners = view.canSeeBankAccountOwners, + can_see_bank_account_swift_bic = view.canSeeBankAccountSwift_bic, + can_see_bank_account_type = view.canSeeBankAccountType, + can_see_comments = view.canSeeComments, can_see_corporate_location = view.canSeeCorporateLocation, + can_see_image_url = view.canSeeImageUrl, + can_see_images = view.canSeeImages, + can_see_more_info = view.canSeeMoreInfo, + can_see_open_corporates_url = view.canSeeOpenCorporatesUrl, + can_see_other_account_bank_name = view.canSeeOtherAccountBankName, + can_see_other_account_iban = view.canSeeOtherAccountIBAN, + can_see_other_account_kind = view.canSeeOtherAccountKind, + can_see_other_account_metadata = view.canSeeOtherAccountMetadata, + can_see_other_account_national_identifier = view.canSeeOtherAccountNationalIdentifier, + can_see_other_account_number = view.canSeeOtherAccountNumber, + can_see_other_account_swift_bic = view.canSeeOtherAccountSWIFT_BIC, + can_see_owner_comment = view.canSeeOwnerComment, can_see_physical_location = view.canSeePhysicalLocation, - can_see_public_alias = view.canSeePublicAlias, can_see_private_alias = view.canSeePrivateAlias, - can_add_more_info = view.canAddMoreInfo, - can_add_url = view.canAddURL, - can_add_image_url = view.canAddImageURL, - can_add_open_corporates_url = view.canAddOpenCorporatesUrl, - can_add_corporate_location = view.canAddCorporateLocation, - can_add_physical_location = view.canAddPhysicalLocation, - can_add_public_alias = view.canAddPublicAlias, - can_add_private_alias = view.canAddPrivateAlias, - can_delete_corporate_location = view.canDeleteCorporateLocation, - can_delete_physical_location = view.canDeletePhysicalLocation, - can_edit_owner_comment = view.canEditOwnerComment, - can_add_comment = view.canAddComment, - can_delete_comment = view.canDeleteComment, - can_add_tag = view.canAddTag, - can_delete_tag = view.canDeleteTag, - can_add_image = view.canAddImage, - can_delete_image = view.canDeleteImage, - can_add_where_tag = view.canAddWhereTag, - can_see_where_tag = view.canSeeWhereTag, - can_delete_where_tag = view.canDeleteWhereTag + can_see_public_alias = view.canSeePublicAlias, + can_see_tags = view.canSeeTags, + can_see_transaction_amount = view.canSeeTransactionAmount, + can_see_transaction_balance = view.canSeeTransactionBalance, + can_see_transaction_currency = view.canSeeTransactionCurrency, + can_see_transaction_description = view.canSeeTransactionDescription, + can_see_transaction_finish_date = view.canSeeTransactionFinishDate, + can_see_transaction_metadata = view.canSeeTransactionMetadata, + can_see_transaction_other_bank_account = view.canSeeTransactionOtherBankAccount, + can_see_transaction_start_date = view.canSeeTransactionStartDate, + can_see_transaction_this_bank_account = view.canSeeTransactionThisBankAccount, + can_see_transaction_type = view.canSeeTransactionType, + can_see_url = view.canSeeUrl, + can_see_where_tag = view.canSeeWhereTag ) } From f35f8b80edbe504eed7af5b83e860c84210d066a Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 21 Jun 2016 11:45:44 +0200 Subject: [PATCH 551/702] Added check if es.metrics.index is list, and throws exception if so. es.warehouse.index can be a list --- src/main/scala/code/search/search.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/search/search.scala b/src/main/scala/code/search/search.scala index 028b05fc0..b39b6c00b 100644 --- a/src/main/scala/code/search/search.scala +++ b/src/main/scala/code/search/search.scala @@ -126,11 +126,13 @@ class elasticsearch extends Loggable { class elasticsearchMetrics extends elasticsearch { - //println("-----------------------------------------------------> elasticsearchMetrics instantiated") override val esHost = Props.get("es.metrics.host","localhost") override val esPortTCP = Props.get("es.metrics.port.tcp","9300") override val esPortHTTP = Props.get("es.metrics.port.http","9200") override val esIndex = Props.get("es.metrics.index", "metrics") + + if (esIndex.contains(",")) throw new RuntimeException("Props error: es.metrics.index can not be a list") + var client:ElasticClient = null if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) { client = ElasticClient.transport("elasticsearch://" + esHost + ":" + esPortTCP + ",") @@ -170,7 +172,6 @@ class elasticsearchMetrics extends elasticsearch { } class elasticsearchWarehouse extends elasticsearch { - //println("-----------------------------------------------------> elasticsearchWarehouse instantiated") override val esHost = Props.get("es.warehouse.host","localhost") override val esPortTCP = Props.get("es.warehouse.port.tcp","9300") override val esPortHTTP = Props.get("es.warehouse.port.http","9200") From 117eb4805466bd0fb901e97c45320da7f5b5555d Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 22 Jun 2016 10:44:58 +0200 Subject: [PATCH 552/702] Added currency to account for kafka connector. getBankAccount refactored once again --- .../scala/code/bankconnectors/Connector.scala | 29 ++++++++++--------- .../bankconnectors/KafkaMappedConnector.scala | 22 +++++++------- .../code/bankconnectors/LocalConnector.scala | 6 ++-- .../bankconnectors/LocalMappedConnector.scala | 10 +++---- .../code/api/v1_3_0/PhysicalCardsTest.scala | 2 +- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 5e1aefbf5..15e0ed84b 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -77,16 +77,17 @@ trait Connector { //gets banks handled by this connector def getBanks : List[Bank] - def getBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] = - getSingleBankAccount(bankId, accountId) - def getBankAccounts(accounts: List[(BankId, AccountId)]) : List[BankAccount] = { for ( acc <- accounts ) yield { - getBankAccount(acc._1, acc._2).orNull - } + for { + a <- getBankAccount(acc._1, acc._2) + } yield { + a + } + }.orNull } - protected def getSingleBankAccount(bankId : BankId, accountId : AccountId) : Box[AccountType] + def getBankAccount(bankId : BankId, accountId : AccountId) : Box[AccountType] def getOtherBankAccount(bankId: BankId, accountID : AccountId, otherAccountID : String) : Box[OtherBankAccount] @@ -117,10 +118,10 @@ trait Connector { def makePayment(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID, amt : BigDecimal, description : String) : Box[TransactionId] = { for{ - fromAccount <- getSingleBankAccount(fromAccountUID.bankId, fromAccountUID.accountId) ?~ + fromAccount <- getBankAccount(fromAccountUID.bankId, fromAccountUID.accountId) ?~ s"account ${fromAccountUID.accountId} not found at bank ${fromAccountUID.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") - toAccount <- getSingleBankAccount(toAccountUID.bankId, toAccountUID.accountId) ?~ + toAccount <- getBankAccount(toAccountUID.bankId, toAccountUID.accountId) ?~ s"account ${toAccountUID.accountId} not found at bank ${toAccountUID.bankId}" sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { s"Cannot send payment to account with different currency (From ${fromAccount.currency} to ${toAccount.currency}" @@ -143,10 +144,10 @@ trait Connector { def makePaymentv200(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID, amt : BigDecimal, description : String) : Box[TransactionId] = { for { - fromAccount <- getSingleBankAccount(fromAccountUID.bankId, fromAccountUID.accountId) ?~ + fromAccount <- getBankAccount(fromAccountUID.bankId, fromAccountUID.accountId) ?~ s"account ${fromAccountUID.accountId} not found at bank ${fromAccountUID.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccountUID.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true, ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) - toAccount <- getSingleBankAccount(toAccountUID.bankId, toAccountUID.accountId) ?~ + toAccount <- getBankAccount(toAccountUID.bankId, toAccountUID.accountId) ?~ s"account ${toAccountUID.accountId} not found at bank ${toAccountUID.bankId}" //sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { // s"Cannot send payment to account with different currency (From ${fromAccount.currency} to ${toAccount.currency}" @@ -194,10 +195,10 @@ trait Connector { //create a new transaction request var result = for { - fromAccountType <- getSingleBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ + fromAccountType <- getBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") - toAccountType <- getSingleBankAccount(toAccount.bankId, toAccount.accountId) ?~ + toAccountType <- getBankAccount(toAccount.bankId, toAccount.accountId) ?~ s"account ${toAccount.accountId} not found at bank ${toAccount.bankId}" rawAmt <- tryo { BigDecimal(body.value.amount) } ?~! s"amount ${body.value.amount} not convertible to number" sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { @@ -254,10 +255,10 @@ trait Connector { // Always create a new Transaction Request var result = for { - fromAccountType <- getSingleBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ + fromAccountType <- getBankAccount(fromAccount.bankId, fromAccount.accountId) ?~ s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) - toAccountType <- getSingleBankAccount(toAccount.bankId, toAccount.accountId) ?~ + toAccountType <- getBankAccount(toAccount.bankId, toAccount.accountId) ?~ s"account ${toAccount.accountId} not found at bank ${toAccount.bankId}" rawAmt <- tryo { BigDecimal(body.value.amount) } ?~! s"amount ${body.value.amount} not convertible to number" // isValidTransactionRequestType is checked at API layer. Maybe here too. diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 4df452b40..710e07917 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -260,7 +260,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable return Full(res) } - override def getSingleBankAccount(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { + override def getBankAccount(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId @@ -602,7 +602,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable ) //delete account - val account = getSingleBankAccount(bankId, accountID) + val account = getBankAccount(bankId, accountID) val accountDeleted = account match { case acc => true //acc.delete_! //TODO @@ -638,7 +638,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable private def createAccountIfNotExisting(bankId: BankId, accountID: AccountId, accountNumber: String, currency: String, balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = { - getSingleBankAccount(bankId, accountID) match { + getBankAccount(bankId, accountID) match { case Full(a) => logger.info(s"account with id $accountID at bank with id $bankId already exists. No need to create a new one.") a @@ -666,7 +666,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountID override def getAccountByUUID(uuid: String): Box[AccountType] = { - getSingleBankAccount(null, AccountId(uuid)) + getBankAccount(null, AccountId(uuid)) } //cash api requires a call to add a new transaction and update the account balance @@ -729,7 +729,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable //this will be Full(true) if everything went well val result = for { - acc <- getSingleBankAccount(bankId, accountID) + acc <- getBankAccount(bankId, accountID) bank <- getBank(bankId) } yield { //acc.balance = newBalance @@ -821,7 +821,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable bankId <- getBankByNationalIdentifier(bankNationalIdentifier).map(_.bankId) account <- getAccountByNumber(bankId, accountNumber) } yield { - val acc = getSingleBankAccount(bankId, account.accountId) + val acc = getBankAccount(bankId, account.accountId) acc match { case a => true //a.lastUpdate = updateDate //TODO case _ => logger.warn("can't set bank account.lastUpdated because the account was not found"); false @@ -838,7 +838,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable override def updateAccountLabel(bankId: BankId, accountID: AccountId, label: String): Boolean = { //this will be Full(true) if everything went well val result = for { - acc <- getSingleBankAccount(bankId, accountID) + acc <- getBankAccount(bankId, accountID) bank <- getBank(bankId) } yield { //acc.label = label @@ -886,8 +886,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable if (r.details.completed != null) // && r.details.completed.matches("^[0-9]{8}$")) dateCompleted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.completed) - val c = getSingleBankAccount(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).orNull - val o = getSingleBankAccount(BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull + val c = getBankAccount(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).orNull + val o = getBankAccount(BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) val dummyOtherBankAccount = createOtherBankAccount(c, o, None) //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object @@ -898,12 +898,12 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable new Transaction( r.id, // uuid:String TransactionId(r.id), // id:TransactionId - getSingleBankAccount( BankId(r.this_account.bank), + getBankAccount( BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull, // thisAccount:BankAccount otherAccount, // otherAccount:OtherBankAccount r.details.`type`, // transactionType:String BigDecimal(r.details.value), // val amount:BigDecimal - "GBP", // currency:String + o.currency, // currency:String Some(r.details.description), // description:Option[String] datePosted, // startDate:Date dateCompleted, // finishDate:Date diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 492dd24cc..0c91556fc 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -36,7 +36,7 @@ private object LocalConnector extends Connector with Loggable { override def getBanks : List[Bank] = HostedBank.findAll - override def getSingleBankAccount(bankId : BankId, accountId : AccountId) : Box[Account] = { + override def getBankAccount(bankId : BankId, accountId : AccountId) : Box[Account] = { for{ bank <- getHostedBank(bankId) account <- bank.getAccount(accountId) @@ -587,7 +587,7 @@ private object LocalConnector extends Connector with Loggable { //used by the transaction import api override def updateAccountBalance(bankId: BankId, accountId: AccountId, newBalance: BigDecimal): Boolean = { - getSingleBankAccount(bankId, accountId) match { + getBankAccount(bankId, accountId) match { case Full(acc) => acc.accountBalance(newBalance).saveTheRecord().isDefined true @@ -607,7 +607,7 @@ private object LocalConnector extends Connector with Loggable { } override def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean = { - getSingleBankAccount(bankId, accountId) match { + getBankAccount(bankId, accountId) match { case Full(acc) => acc.accountLabel(label).saveTheRecord().isDefined true diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index a27809e36..cd487d292 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -90,7 +90,7 @@ object LocalMappedConnector extends Connector with Loggable { for { bank <- getMappedBank(bankId) - account <- getSingleBankAccount(bankId, accountId) + account <- getBankAccount(bankId, accountId) } { Future{ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) @@ -106,7 +106,7 @@ object LocalMappedConnector extends Connector with Loggable { } // Question: Why is this called getBankAccountType? Why not getBankAccount? TODO rename - override def getSingleBankAccount(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = { + override def getBankAccount(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = { MappedBankAccount.find( By(MappedBankAccount.bank, bankId.value), By(MappedBankAccount.theAccountId, accountId.value)) @@ -428,7 +428,7 @@ object LocalMappedConnector extends Connector with Loggable { private def createAccountIfNotExisting(bankId: BankId, accountId: AccountId, accountNumber: String, currency: String, balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = { - getSingleBankAccount(bankId, accountId) match { + getBankAccount(bankId, accountId) match { case Full(a) => logger.info(s"account with id $accountId at bank with id $bankId already exists. No need to create a new one.") a @@ -517,7 +517,7 @@ object LocalMappedConnector extends Connector with Loggable { //this will be Full(true) if everything went well val result = for { - acc <- getSingleBankAccount(bankId, accountId) + acc <- getBankAccount(bankId, accountId) bank <- getMappedBank(bankId) } yield { acc.accountBalance(Helper.convertToSmallestCurrencyUnits(newBalance, acc.currency)).save @@ -634,7 +634,7 @@ object LocalMappedConnector extends Connector with Loggable { override def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean = { //this will be Full(true) if everything went well val result = for { - acc <- getSingleBankAccount(bankId, accountId) + acc <- getBankAccount(bankId, accountId) bank <- getMappedBank(bankId) } yield { acc.accountLabel(label).save diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index 03373e802..9c0d89c95 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -55,7 +55,7 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { //these methods aren't required by our test override def getBank(bankId : BankId) : Box[Bank] = Empty override def getBanks : List[Bank] = Nil - override def getSingleBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] = Empty + override def getBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] = Empty override def getOtherBankAccount(bankId: BankId, accountID : AccountId, otherAccountID : String) : Box[OtherBankAccount] = Empty override def getOtherBankAccounts(bankId: BankId, accountID : AccountId): List[OtherBankAccount] = From 6017c85c2c4ef17936c3e293046fd27915424139 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 22 Jun 2016 12:01:15 +0200 Subject: [PATCH 553/702] This closes #48 - Write tests for entitlements end points --- .../entitlement/MappedEntitlementTest.scala | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/test/scala/code/entitlement/MappedEntitlementTest.scala diff --git a/src/test/scala/code/entitlement/MappedEntitlementTest.scala b/src/test/scala/code/entitlement/MappedEntitlementTest.scala new file mode 100644 index 000000000..8d31ff111 --- /dev/null +++ b/src/test/scala/code/entitlement/MappedEntitlementTest.scala @@ -0,0 +1,100 @@ +package code.entitlement + +import code.api.ServerSetup +import code.api.util.ApiRole._ +import net.liftweb.mapper.By +import net.liftweb.common.{Full} + +class MappedEntitlementTest extends ServerSetup { + + val userId1 = "833b549e-50e8-49d3-9dcd-9dcdd18c26ec" + val userId2 = "c562a9fa-85b3-41f3-9430-34c7153cc663" + val bankId1 = "obp-bank-test1" + val bankId2 = "obp-bank-test2" + val role1 = CanCreateAccount + + def createEntitlement(bankId: String, userId: String, roleName: String) = MappedEntitlement.create + .mBankId(bankId) + .mUserId(userId) + .mRoleName(roleName) + .saveMe() + + + private def delete() { + MappedEntitlement.bulkDelete_!!() + } + + override def beforeAll() = { + super.beforeAll() + delete() + } + + override def afterEach() = { + super.afterEach() + delete() + } + + feature("Getting Entitlement data") { + scenario("We try to get Entitlement") { + Given("There is no user to customer link at all but we try to get it") + MappedEntitlement.findAll().size should equal(0) + + When("We try to get it all") + val found = MappedEntitlement.getEntitlements.openOr(List()) + + Then("We don't") + found.size should equal(0) + } + } + + scenario("A Entitlement exists for user and we try to get it") { + val entitlement1 = createEntitlement(bankId1, userId1, role1.toString) + Given("Create a user to customer link") + MappedEntitlement.find( + By(MappedEntitlement.mBankId, bankId1), + By(MappedEntitlement.mUserId, userId1), + By(MappedEntitlement.mRoleName, role1.toString) + ).isDefined should equal(true) + + When("We try to get it by user and customer") + val foundOpt = MappedEntitlement.getEntitlement(bankId1, userId1, role1.toString) + + Then("We do") + foundOpt.isDefined should equal(true) + + And("It is the right thing") + val foundThing = foundOpt.get + foundThing should equal(entitlement1) + + And("Primary id should be UUID") + foundThing.entitlementId.filter(_ != '-').size should equal(32) + } + + + scenario("We try to get all Entitlement rows and then delete they"){ + val entitlement1 = createEntitlement(bankId1, userId1, role1.toString) + val entitlement2 = createEntitlement(bankId2, userId2, role1.toString) + + When("We try to get it all") + val found = MappedEntitlement.getEntitlements.openOr(List()) + + Then("We don't") + found.size should equal(2) + + And("We try to get it by user1 and bank1") + val foundThing1 = found.filter(_.userId == userId1).filter(_.bankId == bankId1).filter(_.roleName == role1.toString) + foundThing1 should equal(List(entitlement1)) + + And("We try to get it by user2 and bank2") + val foundThing2 = found.filter(_.userId == userId2).filter(_.bankId == bankId2).filter(_.roleName == role1.toString) + foundThing2 should equal(List(entitlement2)) + + And("We try to delete all rows") + found.foreach { + d => { + MappedEntitlement.deleteEntitlement(Full(d)) should equal(Full(true)) + } + } + } + +} From 75513efd57269ac6b707134c24fadf2e2f97b2a9 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 22 Jun 2016 14:34:29 +0200 Subject: [PATCH 554/702] Fixed getBankAccounts --- src/main/scala/code/bankconnectors/Connector.scala | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 15e0ed84b..01703cc61 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -78,13 +78,10 @@ trait Connector { def getBanks : List[Bank] def getBankAccounts(accounts: List[(BankId, AccountId)]) : List[BankAccount] = { - for ( acc <- accounts ) yield { - for { - a <- getBankAccount(acc._1, acc._2) - } yield { - a - } - }.orNull + for { + acc <- accounts + a <- getBankAccount(acc._1, acc._2) + } yield a } def getBankAccount(bankId : BankId, accountId : AccountId) : Box[AccountType] From 685983939f52bbc80348f401cf088e3b8b48ddc6 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 22 Jun 2016 14:53:36 +0200 Subject: [PATCH 555/702] Write tests for entitlements end points #48 - fixed info text --- .../scala/code/entitlement/MappedEntitlementTest.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/scala/code/entitlement/MappedEntitlementTest.scala b/src/test/scala/code/entitlement/MappedEntitlementTest.scala index 8d31ff111..7303dd10c 100644 --- a/src/test/scala/code/entitlement/MappedEntitlementTest.scala +++ b/src/test/scala/code/entitlement/MappedEntitlementTest.scala @@ -36,7 +36,7 @@ class MappedEntitlementTest extends ServerSetup { feature("Getting Entitlement data") { scenario("We try to get Entitlement") { - Given("There is no user to customer link at all but we try to get it") + Given("There is no entitlements at all but we try to get it") MappedEntitlement.findAll().size should equal(0) When("We try to get it all") @@ -48,15 +48,15 @@ class MappedEntitlementTest extends ServerSetup { } scenario("A Entitlement exists for user and we try to get it") { + Given("Create an entitlement") val entitlement1 = createEntitlement(bankId1, userId1, role1.toString) - Given("Create a user to customer link") MappedEntitlement.find( By(MappedEntitlement.mBankId, bankId1), By(MappedEntitlement.mUserId, userId1), By(MappedEntitlement.mRoleName, role1.toString) ).isDefined should equal(true) - When("We try to get it by user and customer") + When("We try to get it by bank, user and role") val foundOpt = MappedEntitlement.getEntitlement(bankId1, userId1, role1.toString) Then("We do") @@ -81,11 +81,11 @@ class MappedEntitlementTest extends ServerSetup { Then("We don't") found.size should equal(2) - And("We try to get it by user1 and bank1") + And("We try to get it by user1, bank1 and role1") val foundThing1 = found.filter(_.userId == userId1).filter(_.bankId == bankId1).filter(_.roleName == role1.toString) foundThing1 should equal(List(entitlement1)) - And("We try to get it by user2 and bank2") + And("We try to get it by user2, bank2 and role2") val foundThing2 = found.filter(_.userId == userId2).filter(_.bankId == bankId2).filter(_.roleName == role1.toString) foundThing2 should equal(List(entitlement2)) From 0bdb7a4430f5326bce46a7409661885653d1c039 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 22 Jun 2016 16:16:37 +0200 Subject: [PATCH 556/702] Add bank level entitlements / roles #42 - fixed task 6 --- src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala index 66b0ef3ab..3d1eece20 100644 --- a/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala +++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala @@ -315,7 +315,7 @@ case class TransactionRequestBodyJSON ( ) case class CreateEntitlementJSON(bank_id: String, role_name: String) -case class EntitlementJSON(entitlement_id: String, user_id: String, role_name: String) +case class EntitlementJSON(entitlement_id: String, role_name: String, bank_id: String) case class EntitlementJSONs(list: List[EntitlementJSON]) object JSONFactory200{ @@ -776,8 +776,8 @@ def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTy def createEntitlementJSON(e: Entitlement): EntitlementJSON = { EntitlementJSON(entitlement_id = e.entitlementId, - user_id = e.userId, - role_name = e.roleName) + role_name = e.roleName, + bank_id = e.bankId) } def createEntitlementJSONs(l: List[Entitlement]) = { From 4bd5992611c21adc75db35f2e02c4ce021175aeb Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 22 Jun 2016 16:28:19 +0200 Subject: [PATCH 557/702] This closes #66 - Add a guard at addEntitlement endpoint for non existing bank --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index f47663a7e..98e573445 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1705,6 +1705,7 @@ trait APIMethods200 { isSuperAdmin <- booleanToBox(isSuperAdmin(u.userId)) ?~ "Logged user is not super admin!" user <- User.findByUserId(userId) ?~! ErrorMessages.UserNotFoundById postedData <- tryo{json.extract[CreateEntitlementJSON]} ?~ "wrong format JSON" + bank <- tryo(Bank(BankId(postedData.bank_id)).get) ?~! {ErrorMessages.BankNotFound} role <- tryo{valueOf(postedData.role_name)} ?~! "wrong role name" hasEntitlement <- booleanToBox(hasEntitlement(postedData.bank_id, userId, role) == false, "Entitlement already exists for the user.") addedEntitlement <- Entitlement.entitlement.vend.addEntitlement(postedData.bank_id, userId, postedData.role_name) From 1ec33978a94f43d3b71f23c652aa260bb26c2b68 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 22 Jun 2016 17:04:08 +0200 Subject: [PATCH 558/702] Add a guard at addEntitlement endpoint for non existing bank #66 - accept when bank_id is empty I.e. system level entitlements --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 98e573445..e9cccec60 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1705,7 +1705,7 @@ trait APIMethods200 { isSuperAdmin <- booleanToBox(isSuperAdmin(u.userId)) ?~ "Logged user is not super admin!" user <- User.findByUserId(userId) ?~! ErrorMessages.UserNotFoundById postedData <- tryo{json.extract[CreateEntitlementJSON]} ?~ "wrong format JSON" - bank <- tryo(Bank(BankId(postedData.bank_id)).get) ?~! {ErrorMessages.BankNotFound} + bank <- booleanToBox(Bank(BankId(postedData.bank_id)).isEmpty == false || postedData.bank_id.nonEmpty == false) ?~! {ErrorMessages.BankNotFound} role <- tryo{valueOf(postedData.role_name)} ?~! "wrong role name" hasEntitlement <- booleanToBox(hasEntitlement(postedData.bank_id, userId, role) == false, "Entitlement already exists for the user.") addedEntitlement <- Entitlement.entitlement.vend.addEntitlement(postedData.bank_id, userId, postedData.role_name) From ac3fee3c1a3220ea6074c69c02081f486899c1c1 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Fri, 24 Jun 2016 09:17:35 +0200 Subject: [PATCH 559/702] Added override stylesheet for SocGen --- src/main/webapp/media/css/overrides/socgen.css | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/webapp/media/css/overrides/socgen.css diff --git a/src/main/webapp/media/css/overrides/socgen.css b/src/main/webapp/media/css/overrides/socgen.css new file mode 100644 index 000000000..f70eb8213 --- /dev/null +++ b/src/main/webapp/media/css/overrides/socgen.css @@ -0,0 +1,6 @@ +#authorizeSection, +#registerAppSection, +#create-sandbox-account, +#header-decoration { + background-color: #000000 !important; +} From 937cba4e06d9b83482d17d2a650b297223ae0d1b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 25 Jun 2016 02:33:40 +0100 Subject: [PATCH 560/702] Updating README on Sandbox population --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 23840af4f..cfbfcd238 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,15 @@ The current workaround is to move the project directory onto a different partiti The default database for testing etc is H2. PostgreSQL is used for the sandboxes (user accounts, metadata, transaction cache). +## Sandbox data + +To populate the OBP database with sandbox data: + +1) In your Props file, set allow_sandbox_data_import=true +2) In your Props files, set sandbox_data_import_secret=YOUR-KEY-HERE +3) Now you can POST the sandbox json found in src/main/scala/code/api/sandbox/example_data/example_import.json to /sandbox/v1.0/data-import?secret_token=YOUR-KEY-HERE +4) If successful you should get 201 Created. + ## Kafka (optional): From 624d011446c478d09e39d3c3e01686f61af1acb1 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 25 Jun 2016 02:34:00 +0100 Subject: [PATCH 561/702] Transaction Request documentation --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index f47663a7e..382a7de8f 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1122,7 +1122,7 @@ trait APIMethods200 { | |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL | - |The payee is set in the request body. Money goes into the BANK_ID and ACCOUNT_ID specified in the request body. + |The payee is set in the request body. Money goes into the BANK_ID and ACCOUNT_IDO specified in the request body. | | | From 61cacbde45b79a1446fdb2036a2576f4ffb9ef49 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 25 Jun 2016 03:23:48 +0100 Subject: [PATCH 562/702] Making apiTag variable names and strings singular --- src/main/scala/code/api/util/APIUtil.scala | 20 +-- .../scala/code/api/v1_2_1/APIMethods121.scala | 138 +++++++++--------- .../scala/code/api/v1_4_0/APIMethods140.scala | 6 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 26 ++-- 4 files changed, 96 insertions(+), 94 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index b432e6b2a..66d9dd324 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -359,24 +359,26 @@ object APIUtil extends Loggable { // Used to tag Resource Docs case class ResourceDocTag(tag: String) - val apiTagPayment = ResourceDocTag("Payments") + // Use the *singular* case. for both the variable name and string. + // e.g. "This call is Payment related" + val apiTagPayment = ResourceDocTag("Payment") val apiTagApiInfo = ResourceDocTag("APIInfo") - val apiTagBanks = ResourceDocTag("Banks") - val apiTagAccounts = ResourceDocTag("Accounts") + val apiTagBank = ResourceDocTag("Bank") + val apiTagAccount = ResourceDocTag("Account") val apiTagPublicData = ResourceDocTag("PublicData") val apiTagPrivateData = ResourceDocTag("PrivateData") - val apiTagTransactions = ResourceDocTag("Transactions") - val apiTagMetaData = ResourceDocTag("Meta Data") - val apiTagViews = ResourceDocTag("Views") - val apiTagEntitlements = ResourceDocTag("Entitlements") + val apiTagTransaction = ResourceDocTag("Transaction") + val apiTagMetaData = ResourceDocTag("MetaData") + val apiTagView = ResourceDocTag("View") + val apiTagEntitlement = ResourceDocTag("Entitlement") val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired") - val apiTagCounterparties = ResourceDocTag("Counterparties") + val apiTagCounterparty = ResourceDocTag("Counterparty") val apiTagKyc = ResourceDocTag("KYC") val apiTagCustomer = ResourceDocTag("Customer") val apiTagOnboarding = ResourceDocTag("Onboarding") val apiTagUser = ResourceDocTag("User") val apiTagMeeting = ResourceDocTag("Meeting") - val apiTagExperimental = ResourceDocTag("Experimental!") + val apiTagExperimental = ResourceDocTag("Experimental") // Used to document the API calls diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index aa28ee20c..05198e5d6 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -133,7 +133,7 @@ trait APIMethods121 { true, false, true, - apiTagBanks :: Nil) + apiTagBank :: Nil) lazy val getBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get banks @@ -170,7 +170,7 @@ trait APIMethods121 { true, false, true, - apiTagBanks :: Nil) + apiTagBank :: Nil) lazy val bankById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -211,7 +211,7 @@ trait APIMethods121 { true, true, true, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for all banks (private + public) @@ -238,7 +238,7 @@ trait APIMethods121 { true, true, true, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for all banks @@ -268,7 +268,7 @@ trait APIMethods121 { false, false, false, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val publicAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for all banks @@ -301,7 +301,7 @@ trait APIMethods121 { false, false, false, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val allAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get accounts for a single bank (private + public) @@ -333,7 +333,7 @@ trait APIMethods121 { true, true, true, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val privateAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private accounts for a single bank @@ -365,7 +365,7 @@ trait APIMethods121 { false, false, false, - apiTagAccounts :: apiTagPublicData :: Nil) + apiTagAccount :: apiTagPublicData :: Nil) lazy val publicAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for a single bank @@ -405,7 +405,7 @@ trait APIMethods121 { false, true, false, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val accountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get account by id @@ -438,7 +438,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagMetaData)) + List(apiTagAccount, apiTagMetaData)) lazy val updateAccountLabel : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //change account label @@ -494,7 +494,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews)) + List(apiTagAccount, apiTagView)) lazy val getViewsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get the available views on an bank account @@ -537,7 +537,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews)) + List(apiTagAccount, apiTagView)) lazy val createViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //creates a view on an bank account @@ -574,7 +574,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews)) + List(apiTagAccount, apiTagView)) lazy val updateViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //updates a view on a bank account @@ -606,7 +606,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews)) + List(apiTagAccount, apiTagView)) lazy val deleteViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //deletes a view on an bank account @@ -636,7 +636,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements) + List(apiTagAccount, apiTagView, apiTagEntitlement) ) lazy val getPermissionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -671,7 +671,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements)) + List(apiTagAccount, apiTagView, apiTagEntitlement)) lazy val getPermissionForUserForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get access for specific user @@ -706,7 +706,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) + List(apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired)) lazy val addPermissionForUserForBankAccountForMultipleViews : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a list of views @@ -742,7 +742,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) + List(apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired)) lazy val addPermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a specific view @@ -778,7 +778,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) + List(apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired)) lazy val removePermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete access for specific user to one view @@ -809,7 +809,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements, apiTagOwnerRequired)) + List(apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired)) lazy val removePermissionForUserForBankAccountForAllViews : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete access for specific user to all the views @@ -840,7 +840,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagCounterparties)) + List(apiTagAccount, apiTagCounterparty)) lazy val getCounterpartiesForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get other accounts for one account @@ -873,7 +873,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccounts, apiTagCounterparties)) + List(apiTagAccount, apiTagCounterparty)) lazy val getCounterpartyByIdForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get one other account by id @@ -906,7 +906,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val getCounterpartyMetadata : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get metadata of one other account @@ -940,7 +940,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val getCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public alias of other bank account @@ -980,7 +980,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add public alias to other bank account @@ -1016,7 +1016,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update public alias of other bank account @@ -1052,7 +1052,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete public alias of other bank account @@ -1085,7 +1085,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val getCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get private alias of other bank account @@ -1120,7 +1120,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add private alias to other bank account @@ -1157,7 +1157,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update private alias of other bank account @@ -1194,7 +1194,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete private alias of other bank account @@ -1227,7 +1227,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add more info to other bank account @@ -1262,7 +1262,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update more info of other bank account @@ -1297,7 +1297,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete more info of other bank account @@ -1330,7 +1330,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1366,7 +1366,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update url of other bank account @@ -1401,7 +1401,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete url of other bank account @@ -1434,7 +1434,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add image url to other bank account @@ -1469,7 +1469,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update image url of other bank account @@ -1504,7 +1504,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete image url of other bank account @@ -1537,7 +1537,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add open corporate url to other bank account @@ -1572,7 +1572,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update open corporate url of other bank account @@ -1607,7 +1607,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete open corporate url of other bank account @@ -1640,7 +1640,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add corporate location to other bank account @@ -1677,7 +1677,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update corporate location of other bank account @@ -1714,7 +1714,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete corporate location of other bank account @@ -1752,7 +1752,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val addCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add physical location to other bank account @@ -1789,7 +1789,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val updateCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update physical location to other bank account @@ -1826,7 +1826,7 @@ trait APIMethods121 { false, false, false, - List(apiTagCounterparties, apiTagMetaData)) + List(apiTagCounterparty, apiTagMetaData)) lazy val deleteCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete physical location of other bank account @@ -1875,7 +1875,7 @@ trait APIMethods121 { false, true, false, - List(apiTagAccounts, apiTagTransactions)) + List(apiTagAccount, apiTagTransaction)) lazy val getTransactionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get transactions @@ -1910,7 +1910,7 @@ trait APIMethods121 { false, true, false, - List(apiTagAccounts, apiTagTransactions)) + List(apiTagAccount, apiTagTransaction)) lazy val getTransactionByIdForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get transaction by id @@ -1943,7 +1943,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val getTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get narrative @@ -1975,7 +1975,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val addTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add narrative @@ -2010,7 +2010,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val updateTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update narrative @@ -2045,7 +2045,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val deleteTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete narrative @@ -2077,7 +2077,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val getCommentsForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get comments @@ -2109,7 +2109,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val addCommentForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add comment @@ -2145,7 +2145,7 @@ trait APIMethods121 { false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val deleteCommentForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete comment @@ -2177,7 +2177,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val getTagsForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get tags @@ -2209,7 +2209,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val addTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add a tag @@ -2246,7 +2246,7 @@ Authentication via OAuth is required. The user must either have owner privileges false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val deleteTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete a tag @@ -2279,7 +2279,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val getImagesForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get images @@ -2311,7 +2311,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val addImageForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add an image @@ -2346,7 +2346,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val deleteImageForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete an image @@ -2379,7 +2379,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val getWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get where tag @@ -2412,7 +2412,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val addWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add where tag @@ -2449,7 +2449,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val updateWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //update where tag @@ -2486,7 +2486,7 @@ Authentication via OAuth is required. The user must either have owner privileges false, false, false, - List(apiTagTransactions, apiTagMetaData)) + List(apiTagTransaction, apiTagMetaData)) lazy val deleteWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //delete where tag @@ -2522,7 +2522,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagTransactions, apiTagCounterparties)) + List(apiTagTransaction, apiTagCounterparty)) lazy val getCounterpartyForTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get other account of a transaction diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index b85d056c8..7102bd2ba 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -193,7 +193,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, false, true, - List(apiTagBanks) + List(apiTagBank) ) lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -239,7 +239,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, false, true, - List(apiTagBanks) + List(apiTagBank) ) lazy val getAtms : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -293,7 +293,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, false, true, - List(apiTagBanks) + List(apiTagBank) ) lazy val getProducts : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 747025981..635037ad6 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -144,7 +144,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts, apiTagPrivateData, apiTagPublicData)) + List(apiTagAccount, apiTagPrivateData, apiTagPublicData)) lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -172,7 +172,7 @@ trait APIMethods200 { true, true, true, - List(apiTagAccounts, apiTagPrivateData)) + List(apiTagAccount, apiTagPrivateData)) apiRelations += ApiRelation(privateAccountsAllBanks, getCoreAccountById, "detail") @@ -213,7 +213,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts, apiTagPublicData)) + List(apiTagAccount, apiTagPublicData)) @@ -250,7 +250,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts, apiTagPrivateData, apiTagPublicData) + List(apiTagAccount, apiTagPrivateData, apiTagPublicData) ) lazy val allAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -283,7 +283,7 @@ trait APIMethods200 { true, true, true, - List(apiTagAccounts, apiTagPrivateData)) + List(apiTagAccount, apiTagPrivateData)) def privateAccountsAtOneBankResult (bank: Bank, u: User) = { @@ -343,7 +343,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts, apiTagPublicData)) + List(apiTagAccount, apiTagPublicData)) lazy val publicAccountsAtOneBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get public accounts for a single bank @@ -761,7 +761,7 @@ trait APIMethods200 { true, true, false, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val getCoreAccountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get account by id (assume owner view requested) @@ -814,7 +814,7 @@ trait APIMethods200 { true, true, true, - List(apiTagAccounts, apiTagTransactions)) + List(apiTagAccount, apiTagTransaction)) lazy val getCoreTransactionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get transactions @@ -866,7 +866,7 @@ trait APIMethods200 { false, true, false, - apiTagAccounts :: Nil) + apiTagAccount :: Nil) lazy val accountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get account by id @@ -907,7 +907,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements) + List(apiTagAccount, apiTagView, apiTagEntitlement) ) lazy val getPermissionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -943,7 +943,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts, apiTagViews, apiTagEntitlements)) + List(apiTagAccount, apiTagView, apiTagEntitlement)) lazy val getPermissionForUserForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //get access for specific user @@ -977,7 +977,7 @@ trait APIMethods200 { false, false, false, - List(apiTagAccounts) + List(apiTagAccount) ) apiRelations += ApiRelation(createAccount, createAccount, "self") @@ -1053,7 +1053,7 @@ trait APIMethods200 { true, true, false, - List(apiTagBanks) + List(apiTagBank) ) lazy val getTransactionTypes : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { From 279df2e18b6df4f7c82f47e0513a5e62d54fefa7 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 25 Jun 2016 13:42:14 +0200 Subject: [PATCH 563/702] Refactored MappedTransaction, added KafkaTransaction placeholder --- src/main/scala/bootstrap/liftweb/Boot.scala | 1 + .../bankconnectors/KafkaMappedConnector.scala | 3 ++- .../code/bankconnectors/LocalConnector.scala | 2 +- .../bankconnectors/LocalMappedConnector.scala | 1 + .../scala/code/model/dataAccess/Account.scala | 23 +++++++------------ .../LocalMappedConnectorDataImport.scala | 13 +++++------ .../MappedTransaction.scala | 8 +++---- .../code/api/LocalConnectorTestSetup.scala | 4 ---- .../api/LocalMappedConnectorTestSetup.scala | 3 ++- 9 files changed, 25 insertions(+), 33 deletions(-) rename src/main/scala/code/{model => transaction}/MappedTransaction.scala (97%) diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 7babd4194..0ee364942 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -73,6 +73,7 @@ import net.liftweb.sitemap._ import net.liftweb.util.Helpers._ import net.liftweb.util.{Helpers, Schedule, _} import code.api.Constant._ +import code.transaction.MappedTransaction /** diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 710e07917..e14368c9f 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -14,8 +14,9 @@ import code.model._ import code.model.dataAccess._ import code.sandbox.{CreateViewImpls, Saveable} import code.tesobe.CashTransaction +import code.transaction.{KafkaTransaction, MappedTransaction} import code.transactionrequests.MappedTransactionRequest -import code.transactionrequests.TransactionRequests.{TransactionRequestCharge, TransactionRequest, TransactionRequestBody, TransactionRequestChallenge} +import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} import code.util.Helper import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge} import code.util.{Helper, TTLCache} diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 0c91556fc..307251111 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -21,8 +21,8 @@ import net.liftweb.util.Helpers._ import net.liftweb.util.Props import org.bson.types.ObjectId -import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent._ import scala.math.BigDecimal.RoundingMode private object LocalConnector extends Connector with Loggable { diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index cd487d292..8cf580b30 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -13,6 +13,7 @@ import code.metadata.wheretags.MappedWhereTag import code.model._ import code.model.dataAccess._ import code.tesobe.CashTransaction +import code.transaction.MappedTransaction import code.transactionrequests.MappedTransactionRequest import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} import code.util.Helper diff --git a/src/main/scala/code/model/dataAccess/Account.scala b/src/main/scala/code/model/dataAccess/Account.scala index a321ff2a0..5f504f30a 100644 --- a/src/main/scala/code/model/dataAccess/Account.scala +++ b/src/main/scala/code/model/dataAccess/Account.scala @@ -33,23 +33,16 @@ Berlin 13359, Germany package code.model.dataAccess import java.util.Date -import com.mongodb.QueryBuilder -import net.liftweb.mongodb.record.MongoMetaRecord -import net.liftweb.mongodb.record.field.ObjectIdPk -import net.liftweb.mongodb.record.MongoRecord -import net.liftweb.mongodb.record.field.ObjectIdRefField -import net.liftweb.mongodb.record.field.DateField -import net.liftweb.common._ -import net.liftweb.record.field.{ StringField, BooleanField, DecimalField } -import net.liftweb.mongodb.{Limit, Skip} + +import code.bankconnectors.{OBPLimit, OBPOffset, OBPOrdering, _} import code.model._ +import com.mongodb.QueryBuilder +import net.liftweb.common._ import net.liftweb.mongodb.BsonDSL._ -import code.bankconnectors._ -import code.bankconnectors.OBPOffset -import code.bankconnectors.OBPLimit -import code.bankconnectors.OBPOrdering -import net.liftweb.mongodb.Limit -import net.liftweb.mongodb.Skip +import net.liftweb.mongodb.{Limit, Skip} +import net.liftweb.mongodb.record.{MongoMetaRecord, MongoRecord} +import net.liftweb.mongodb.record.field.{DateField, ObjectIdPk, ObjectIdRefField} +import net.liftweb.record.field.{DecimalField, StringField} class Account extends BankAccount with MongoRecord[Account] with ObjectIdPk[Account] with Loggable{ diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala index 99bfa9aba..9d6ca64ae 100644 --- a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -1,18 +1,17 @@ package code.sandbox import code.atms.MappedAtm -import code.crm.CrmEvent.CrmEvent +import code.branches.MappedBranch import code.crm.MappedCrmEvent -import code.metadata.counterparties.{MappedCounterpartyMetadata} -import code.model.dataAccess.{MappedBankAccount, MappedBank} -import code.model.{MappedTransaction, AccountId, BankId} -import code.branches.{MappedBranch} +import code.metadata.counterparties.MappedCounterpartyMetadata +import code.model.dataAccess.{MappedBank, MappedBankAccount} +import code.model.{AccountId, BankId} import code.products.MappedProduct -import code.products.Products.ProductCode +import code.transaction.MappedTransaction // , MappedDataLicense import code.util.Helper.convertToSmallestCurrencyUnits -import net.liftweb.common.{Full, Failure, Box} +import net.liftweb.common.{Box, Failure, Full} import net.liftweb.mapper.Mapper import net.liftweb.util.Helpers._ diff --git a/src/main/scala/code/model/MappedTransaction.scala b/src/main/scala/code/transaction/MappedTransaction.scala similarity index 97% rename from src/main/scala/code/model/MappedTransaction.scala rename to src/main/scala/code/transaction/MappedTransaction.scala index 2acb70a18..ffd5e90b8 100644 --- a/src/main/scala/code/model/MappedTransaction.scala +++ b/src/main/scala/code/transaction/MappedTransaction.scala @@ -1,12 +1,12 @@ -package code.model +package code.transaction import java.util.UUID import code.bankconnectors.Connector -import code.metadata.counterparties.Counterparties -import code.util.{MappedAccountNumber, DefaultStringField, Helper, MappedUUID} -import net.liftweb.common.{Logger, Box} +import code.util.{DefaultStringField, Helper, MappedAccountNumber, MappedUUID} +import net.liftweb.common.Logger import net.liftweb.mapper._ +import code.model._ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK with CreatedUpdated with TransactionUUID { diff --git a/src/test/scala/code/api/LocalConnectorTestSetup.scala b/src/test/scala/code/api/LocalConnectorTestSetup.scala index 600fd70b4..b476663fe 100644 --- a/src/test/scala/code/api/LocalConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalConnectorTestSetup.scala @@ -2,13 +2,9 @@ package code.api import java.util.Date -import bootstrap.liftweb.ToSchemify -import code.bankconnectors.{OBPLimit, OBPOffset, Connector} import code.model._ import code.model.dataAccess._ import com.mongodb.QueryBuilder -import net.liftweb.mapper.MetaMapper -import net.liftweb.mongodb._ import net.liftweb.util.Helpers._ import scala.math.BigDecimal diff --git a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala index 53d154b35..ac4a3ef5b 100644 --- a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala @@ -8,7 +8,8 @@ import code.model.dataAccess._ import net.liftweb.common.Box import net.liftweb.mapper.MetaMapper import net.liftweb.util.Helpers._ -import code.entitlement.{MappedEntitlement, Entitlement} +import code.entitlement.{Entitlement, MappedEntitlement} +import code.transaction.MappedTransaction import scala.util.Random From 7608760c49d68ebc0f0ca654c7371249c6d9ab76 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Sat, 25 Jun 2016 13:44:29 +0200 Subject: [PATCH 564/702] Added KafkaTransaction placeholder --- .../code/transaction/KafkaTransaction.scala | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/main/scala/code/transaction/KafkaTransaction.scala diff --git a/src/main/scala/code/transaction/KafkaTransaction.scala b/src/main/scala/code/transaction/KafkaTransaction.scala new file mode 100644 index 000000000..677230f21 --- /dev/null +++ b/src/main/scala/code/transaction/KafkaTransaction.scala @@ -0,0 +1,158 @@ +package code.transaction + +import java.util.UUID + +import code.bankconnectors.Connector +import code.model._ +import net.liftweb.common.Logger + +class KafkaTransaction extends TransactionUUID { + + var _bank = "" + var _account = "" + var _counterpartyIban = "" + + def counterpartyIban: String ={ _counterpartyIban } + + private val logger = Logger(classOf[KafkaTransaction]) + + def bank: String = _bank + def account: String = _account + val transactionId: String = UUID.randomUUID().toString + + + override def theTransactionId = TransactionId(transactionId) + override def theAccountId = AccountId(account) + override def theBankId = BankId(bank) + + def getCounterpartyIban() = { + val i = counterpartyIban + if(i.isEmpty) None else Some(i) + } + + def toTransaction(account: BankAccount): Option[Transaction] = { + /* val tBankId = theBankId + val tAccId = theAccountId + + if (tBankId != account.bankId || tAccId != account.accountId) { + logger.warn("Attempted to convert KafkaTransaction to Transaction using unrelated existing BankAccount object") + None + } else { + val label = { + val d = description.get + if (d.isEmpty) None else Some(d) + } + + val transactionCurrency = currency.get + val amt = Helper.smallestCurrencyUnitToBigDecimal(amount.get, transactionCurrency) + val newBalance = Helper.smallestCurrencyUnitToBigDecimal(newAccountBalance.get, transactionCurrency) + + def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { + new OtherBankAccount( + id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), + label = counterpartyAccountHolder.get, + nationalIdentifier = counterpartyNationalId.get, + swift_bic = None, //TODO: need to add this to the json/model + iban = getCounterpartyIban(), + number = counterpartyAccountNumber.get, + bankName = counterpartyBankName.get, + kind = counterpartyAccountKind.get, + originalPartyBankId = theBankId, + originalPartyAccountId = theAccountId, + alreadyFoundMetadata = alreadyFoundMetadata + ) + } + + //it's a bit confusing what's going on here, as normally metadata should be automatically generated if + //it doesn't exist when an OtherBankAccount object is created. The issue here is that for legacy reasons + //otherAccount ids are metadata ids, so the metadata needs to exist before we created the OtherBankAccount + //so that we know what id to give it. + + //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) + val dummyOtherBankAccount = createOtherBankAccount(None) + + //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object + //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init + val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata)) + + Some(new Transaction( + transactionUUID.get, + theTransactionId, + account, + otherAccount, + transactionType.get, + amt, + transactionCurrency, + label, + tStartDate.get, + tFinishDate.get, + newBalance)) + } + */ + null + } + + def toTransaction : Option[Transaction] = { + for { + acc <- Connector.connector.vend.getBankAccount(theBankId, theAccountId) + transaction <- toTransaction(acc) + } yield transaction + } + +} + +object KafkaTransaction extends KafkaTransaction { + /* + private val logger = Logger(classOf[KafkaTransaction]) + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transactionId extends MappedString(this, 255) { + override def defaultValue = UUID.randomUUID().toString + } + //TODO: review the need for this + // (why do we need transactionUUID and transactionId - which is a UUID?) + object transactionUUID extends MappedUUID(this) + object transactionType extends MappedString(this, 100) + + //amount/new balance use the smallest unit of currency! e.g. cents, yen, pence, øre, etc. + object amount extends MappedLong(this) + object newAccountBalance extends MappedLong(this) + + object currency extends MappedString(this, 10) // This should probably be 3 only characters long + + object tStartDate extends MappedDateTime(this) + object tFinishDate extends MappedDateTime(this) + + object description extends DefaultStringField(this) + + object counterpartyAccountNumber extends MappedAccountNumber(this) + object counterpartyAccountHolder extends MappedString(this, 100) + //still unclear exactly how what this is defined to mean + object counterpartyNationalId extends MappedString(this, 40) + //this should eventually be calculated using counterpartyNationalId + object counterpartyBankName extends MappedString(this, 100) + //this should eventually either generate counterpartyAccountNumber or be generated + object counterpartyIban extends MappedString(this, 100) + object counterpartyAccountKind extends MappedString(this, 40) + + //This is a holder for storing data from a previous model version that wasn't set correctly + //e.g. some previous models had counterpartyAccountNumber set to a string that was clearly + //not a valid account number, though the string may have actually contained the account number + //somewhere within it (e.g. "BLS 3020201 BLAH BLAH S/C 2014-05-22") + // + // We save information like this so that we can try to manually process it later. + // + // Keep in mind that changing the counterparty account number will require an update + // to the corresponding counterparty metadata object! + @deprecated + object extraInfo extends DefaultStringField(this) + + + override def theTransactionId = TransactionId(transactionId.get) + override def theAccountId = AccountId(account.get) + override def theBankId = BankId(bank.get) +*/ + + +} From c19aa27f277fa44a305ed6fc2108a27579bc9794 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 27 Jun 2016 08:34:51 +0100 Subject: [PATCH 565/702] Adding API Explorer links with Tags to index --- src/main/scala/code/api/util/APIUtil.scala | 2 +- .../scala/code/api/v1_2_1/APIMethods121.scala | 2 +- .../scala/code/api/v1_4_0/APIMethods140.scala | 8 +++---- .../scala/code/api/v2_0_0/APIMethods200.scala | 14 ++++++------- src/main/scala/code/snippet/WebUI.scala | 13 +++++++++++- src/main/webapp/index.html | 21 ++++++++++--------- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 66d9dd324..5c1d14e14 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -361,7 +361,7 @@ object APIUtil extends Loggable { // Use the *singular* case. for both the variable name and string. // e.g. "This call is Payment related" - val apiTagPayment = ResourceDocTag("Payment") + val apiTagTransactionRequest = ResourceDocTag("TransactionRequest") val apiTagApiInfo = ResourceDocTag("APIInfo") val apiTagBank = ResourceDocTag("Bank") val apiTagAccount = ResourceDocTag("Account") diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 05198e5d6..9938264ed 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -2562,7 +2562,7 @@ Authentication via OAuth is required if the view is not public.""", false, false, false, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val makePayment : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: Nil JsonPost json -> _ => { diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 7102bd2ba..1e4c3c62c 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -385,7 +385,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val getTransactionRequestTypes: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -422,7 +422,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => { @@ -476,7 +476,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -526,7 +526,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val answerTransactionRequestChallenge: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 635037ad6..1ddbc2629 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1139,7 +1139,7 @@ trait APIMethods200 { true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -1190,7 +1190,7 @@ trait APIMethods200 { true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val answerTransactionRequestChallenge: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -1259,7 +1259,7 @@ trait APIMethods200 { true, true, true, - List(apiTagPayment)) + List(apiTagTransactionRequest)) lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => { @@ -1655,7 +1655,7 @@ trait APIMethods200 { false, false, false, - List(apiTagCustomer)) + List(apiTagUser, apiTagCustomer)) lazy val createUserCustomerLinks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: "user_customer_links" :: Nil JsonPost json -> _ => { @@ -1735,7 +1735,7 @@ trait APIMethods200 { true, true, true, - List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) + List(apiTagUser, apiTagEntitlement)) lazy val getEntitlements: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1774,7 +1774,7 @@ trait APIMethods200 { true, true, true, - List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) + List(apiTagUser, apiTagEntitlement)) lazy val deleteEntitlement: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { @@ -1810,7 +1810,7 @@ trait APIMethods200 { true, true, true, - List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) + List(apiTagUser, apiTagEntitlement)) lazy val getAllEntitlements: PartialFunction[Req, Box[User] => Box[JsonResponse]] = { diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index fe511bfca..d03a00187 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -33,6 +33,7 @@ Berlin 13359, Germany package code.snippet import net.liftweb.common.Loggable +import net.liftweb.http.S import net.liftweb.util.{CssSel, Props} @@ -60,9 +61,19 @@ class WebUI extends Loggable{ } def apiExplorerLink: CssSel = { - ".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", "")) + val tags = S.attr("tags") openOr "" + ".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", "") + s"?tags=$tags") } + + + + + + + + + // Social Finance (Sofi) def sofiLink: CssSel = { ".sofi-link a [href]" #> scala.xml.Unparsed(Props.get("webui_sofi_url", "")) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 3fe2f62be..11b151985 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -74,14 +74,14 @@ Berlin 13359, Germany

    Accounts

    -

    Access the user's list of accounts and account information such as the balance.

    +

    Access the user's list of accounts and account information such as the balance. Explore...

    Branches, ATMs

    -

    Access the list of branches and ATMs for the specified bank including geolocation and opening hours.

    +

    Access the list of branches and ATMs for the specified bank including geolocation and opening hours. Explore...

    @@ -91,14 +91,14 @@ Berlin 13359, Germany

    Transactions

    -

    Access the transaction history and metadata of accounts.

    +

    Access the transaction history and metadata of accounts. Explore...

    Metadata

    -

    Enrich transactions and counterparties with metadata including geolocations, comments, pictures and tags (e.g. category of spending).

    +

    Enrich transactions and counterparties with metadata including geolocations, comments, pictures and tags (e.g. category of spending). Explore...

    @@ -108,14 +108,14 @@ Berlin 13359, Germany

    Counterparties

    -

    Access the payers & payees of an account including metadata such as their aliases, labels, logos and home pages.

    +

    Access the payers & payees of an account including metadata such as their aliases, labels, logos and home pages. Explore...

    Entitlements

    -

    Enable account holders to grant fine-grained access to third-party users and applications. For instance, a business account might provide auditors with full read-only access whilst coworkers might only see the account balance.

    +

    Enable account holders to grant fine-grained access to third-party users and applications. For instance, a business account might provide auditors with full read-only access whilst coworkers might only see the account balance. Explore...

    @@ -125,14 +125,14 @@ Berlin 13359, Germany

    Customer meetings, messages and video conferencing.

    -

    Enable customer meetings, messages and video conferencing for KYC and CRM operations (uses third party video streaming).

    +

    Enable customer meetings, messages and video conferencing for KYC and CRM operations (uses third party video streaming). Explore...

    Security challenges

    -

    Step up authentication using the generic challenge / response mechanism for sensitive operations e.g. to progress a transfer, the user must supply a mobile TAN

    +

    Step up authentication using the generic challenge / response mechanism for sensitive operations e.g. to progress a transfer, the user must supply a mobile TAN Explore...

    @@ -141,14 +141,15 @@ Berlin 13359, Germany

    Payments & Transaction requests

    -

    Enable transfers. View and confirm charges (as per PSD2).

    +

    Initiate transfers. View and confirm charges (as per PSD2). Explore...

    +

    Onboarding & KYC

    -

    Perform user, customer and account creation. Access Know Your Customer (KYC) documents, media and KYC status.

    +

    Perform user, customer and account creation. Access Know Your Customer (KYC) documents, media and KYC status. Explore...

    From d5dfea545bc28779e6ee733e9ed9aa9d1979afff Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 27 Jun 2016 15:55:44 +0200 Subject: [PATCH 566/702] This closes #67 - /search/metrics/q=abc --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 1ddbc2629..513c33679 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -2004,8 +2004,8 @@ trait APIMethods200 { user => for { u <- user ?~ ErrorMessages.UserNotLoggedIn - b <- Bank.all.headOption //TODO: This is a temp workaround - canSearchMetrics <- Entitlement.entitlement.vend.getEntitlement(b.bankId.toString, u.userId, ApiRole.CanSearchMetrics.toString) ?~ "CanSearchMetrics entitlement required" + b <- tryo{Bank.all.headOption} ?~! {ErrorMessages.BankNotFound} //TODO: This is a temp workaround + canSearchMetrics <- Entitlement.entitlement.vend.getEntitlement(b.get.bankId.value, u.userId, ApiRole.CanSearchMetrics.toString) ?~ "CanSearchMetrics entitlement required" } yield { successJsonResponse(Extraction.decompose(esm.searchProxy(u.userId, queryString))) } From 257dd30ea9ec02bf4d9b8d4a39682a2940ae07b9 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Tue, 28 Jun 2016 10:22:54 +0200 Subject: [PATCH 567/702] This closes #68 - /users/USER_EMAIL --- src/main/scala/code/api/util/APIUtil.scala | 1 + src/main/scala/code/api/v2_0_0/APIMethods200.scala | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 5c1d14e14..165160458 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -77,6 +77,7 @@ object ErrorMessages { val UserNotFoundById = "OBP-20005: User not found by User Id." val UserDoesNotHaveRole = "OBP-20006: User does not have a role " + val UserNotFoundByEmail = "OBP-20007: User not found by email." // Resource related messages diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 513c33679..d523c735a 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1623,10 +1623,10 @@ trait APIMethods200 { user => for { l <- user ?~ ErrorMessages.UserNotLoggedIn - b <- Bank.all.headOption //TODO: This is a temp workaround - canGetAnyUser <- booleanToBox(hasEntitlement(b.bankId.value, l.userId, ApiRole.CanGetAnyUser), "CanGetAnyUser entitlement required") + b <- tryo{Bank.all.headOption} ?~! {ErrorMessages.BankNotFound} //TODO: This is a temp workaround + canGetAnyUser <- booleanToBox(hasEntitlement(b.get.bankId.value, l.userId, ApiRole.CanGetAnyUser), "CanGetAnyUser entitlement required") // Workaround to get userEmail address directly from URI without needing to URL-encode it - u <- OBPUser.getApiUserByEmail(CurrentReq.value.uri.split("/").last) + u <- OBPUser.getApiUserByEmail(CurrentReq.value.uri.split("/").last) ?~! {ErrorMessages.UserNotFoundByEmail} } yield { // Format the data as V2.0.0 json From eacf4ddae81ea63035dbc934c0cce437966006db Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 28 Jun 2016 13:10:21 +0200 Subject: [PATCH 568/702] Added override stylesheet for Osuuspankki --- src/main/webapp/media/css/overrides/op.css | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/webapp/media/css/overrides/op.css diff --git a/src/main/webapp/media/css/overrides/op.css b/src/main/webapp/media/css/overrides/op.css new file mode 100644 index 000000000..ade3445ee --- /dev/null +++ b/src/main/webapp/media/css/overrides/op.css @@ -0,0 +1,6 @@ +#authorizeSection, +#registerAppSection, +#create-sandbox-account, +#header-decoration { + background-color: #ff6a10 !important; +} From abab9fb28d9eff4d3449ec53a2c81d0f9c9a8c32 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Tue, 28 Jun 2016 14:28:44 +0200 Subject: [PATCH 569/702] This closes #69 - /search/warehouse/q=abc --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index d523c735a..63513ffe5 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1915,8 +1915,8 @@ trait APIMethods200 { user => for { u <- user ?~ ErrorMessages.UserNotLoggedIn - b <- Bank.all.headOption //TODO: This is a temp workaround - canSearchWarehouse <- Entitlement.entitlement.vend.getEntitlement(b.bankId.toString, u.userId, ApiRole.CanSearchWarehouse.toString) ?~ "CanSearchWarehouse entitlement required" + b <- tryo{Bank.all.headOption} ?~! {ErrorMessages.BankNotFound} //TODO: This is a temp workaround + canSearchWarehouse <- Entitlement.entitlement.vend.getEntitlement(b.get.bankId.value, u.userId, ApiRole.CanSearchWarehouse.toString) ?~ "CanSearchWarehouse entitlement required" } yield { successJsonResponse(Extraction.decompose(esw.searchProxy(u.userId, queryString))) } From 505a860d8683260da61faa25b19021904400e7f1 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 29 Jun 2016 21:31:47 +0200 Subject: [PATCH 570/702] Promoting v2.0.0 of createCustomer --- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 1 + src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 1e4c3c62c..920a84c14 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -569,6 +569,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ |For now the authenticated user can create at most one linked customer. |Dates need to be in the format 2013-01-21T23:08:00Z |OAuth authentication is required. + |Note: This call is depreciated in favour of v.2.0.0 createCustomer |""", Extraction.decompose(PostCustomerJson("687687678", "Joe David Bloggs", "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 1ddbc2629..1597e0111 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -969,7 +969,7 @@ trait APIMethods200 { "createAccount", "PUT", "/banks/BANK_ID/accounts/NEW_ACCOUNT_ID", - "Create an Account at bank specified by BANK_ID with Id specified by NEW_ACCOUNT_ID", + "Create Account at bank specified by BANK_ID with Id specified by NEW_ACCOUNT_ID", "Note: Type is currently ignored and Amount must be zero. You can update the account label with another call (see updateAccountLabel)", Extraction.decompose(CreateAccountJSON("An user_id","CURRENT", AmountOfMoneyJSON121("EUR", "0"))), emptyObjectJson, diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index ad53eb687..9d449f0a0 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -124,7 +124,7 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w // New in 1.4.0 Implementations1_4_0.getCustomer, - Implementations1_4_0.addCustomer, + // Now in 2.0.0 Implementations1_4_0.addCustomer, Implementations1_4_0.getCustomerMessages, Implementations1_4_0.addCustomerMessage, Implementations1_4_0.getBranches, From 6e66ff0651c8b6f0c11d25978faf373ff6ceac81 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 29 Jun 2016 21:34:08 +0200 Subject: [PATCH 571/702] def apiExplorerLink has ignoredefcat=true --- src/main/scala/code/snippet/WebUI.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index d03a00187..d6da487ad 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -62,7 +62,7 @@ class WebUI extends Loggable{ def apiExplorerLink: CssSel = { val tags = S.attr("tags") openOr "" - ".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", "") + s"?tags=$tags") + ".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", "") + s"?ignoredefcat=true&tags=$tags") } From 69efff820f7835d65bb6feb79b79ea659d2e7c87 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 30 Jun 2016 01:08:16 +0200 Subject: [PATCH 572/702] Shortening Resource Doc summaries --- .../scala/code/api/v1_2_1/APIMethods121.scala | 33 +++++----- .../scala/code/api/v1_4_0/APIMethods140.scala | 9 +-- .../scala/code/api/v2_0_0/APIMethods200.scala | 63 ++++++++++--------- src/main/scala/code/views/MapperViews.scala | 7 +++ 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 9938264ed..dc95dfafc 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -158,8 +158,9 @@ trait APIMethods121 { "bankById", "GET", "/banks/BANK_ID", - "Get the bank specified by BANK_ID", - """Returns information about a single bank specified by BANK_ID including: + "Get Bank", + """Get the bank specified by BANK_ID + |Returns information about a single bank specified by BANK_ID including: | |* Short and full name of bank |* Logo URL @@ -462,12 +463,11 @@ trait APIMethods121 { "getViewsForBankAccount", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/views", - "Get the available views on an bank account.", + "Get Views for Account.", """#Views | - |### Delegation, Entitlements (Permissions) | - |Views in Open Bank Project provide a mechanism for fine grained access control and delegation. 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 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. | @@ -517,7 +517,7 @@ trait APIMethods121 { "createViewForBankAccount", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/views", - "Creates a view on an bank account.", + "Create View.", """#Create a view on bank account | | OAuth authentication is required and the user needs to have access to the owner view. @@ -561,7 +561,7 @@ trait APIMethods121 { "updateViewForBankAccount", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID", - "Updates a view on a bank account.", + "Update View.", """Update an existing view on a bank account | |OAuth authentication is required and the user needs to have access to the owner view. @@ -694,7 +694,7 @@ trait APIMethods121 { "addPermissionForUserForBankAccountForMultipleViews", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/views", - "Add access for specific user to a list of views.", + "Grant User access to a list of views.", """Grants the user USER_ID at their provider PROVIDER_ID access to a list of views at BANK_ID for account ACCOUNT_ID. | |All url parameters must be [%-encoded](http://en.wikipedia.org/wiki/Percent-encoding), which is often especially relevant for USER_ID and PROVIDER_ID. @@ -730,7 +730,7 @@ trait APIMethods121 { "addPermissionForUserForBankAccountForOneView", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/views/VIEW_ID", - "Add access for specific user to a specific view.", + "Grant User access to View.", """Grants the user USER_ID at their provider PROVIDER_ID access to the view VIEW_ID at BANK_ID for account ACCOUNT_ID. All url parameters must be [%-encoded](http://en.wikipedia.org/wiki/Percent-encoding), which is often especially relevant for USER_ID and PROVIDER_ID. | |OAuth authentication is required and the user needs to have access to the owner view. @@ -766,7 +766,7 @@ trait APIMethods121 { "removePermissionForUserForBankAccountForOneView", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/views/VIEW_ID", - "Delete access for specific user to one view.", + "Revoke access to one View.", """Revokes the user USER_ID at their provider PROVIDER_ID access to the view VIEW_ID at BANK_ID for account ACCOUNT_ID. | |Revoking a user access to a public view will return an error message. @@ -799,7 +799,7 @@ trait APIMethods121 { "removePermissionForUserForBankAccountForAllViews", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/views", - "Delete access for specific user to all the views.", + "Revoke access to all Views on Account", """Revokes the user USER_ID at their provider PROVIDER_ID access to all the views at BANK_ID for account ACCOUNT_ID. | |OAuth authentication is required and the user needs to have access to the owner view.""", @@ -896,8 +896,9 @@ trait APIMethods121 { "getCounterpartyMetadata", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata", - "Get metadata of one counterpary (other account).", - """Returns only the metadata about one other bank account (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. + "Get Counterparty Metadata.", + """Get metadata of one counterparty (other account). + |Returns only the metadata about one other bank account (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. | |Authentication via OAuth is required if the view is not public.""", emptyObjectJson, @@ -1706,8 +1707,8 @@ trait APIMethods121 { "deleteCounterpartyCorporateLocation", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/corporate_location", - "Delete corporate location of other bank account.", - "Delete the geolocation of the counterparty's registered address", + "Delete Counterparty Metadata Corporate Location.", + "Delete corporate location of other bank account. Delete the geolocation of the counterparty's registered address", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -1818,8 +1819,8 @@ trait APIMethods121 { "deleteCounterpartyPhysicalLocation", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/physical_location", + "Delete Counterparty Metadata Physical Location.", "Delete physical location of other bank account.", - "", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 920a84c14..1ad0c47fd 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -104,8 +104,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "getCustomerMessages", "GET", "/banks/BANK_ID/customer/messages", - "Get messages for the logged in customer", - """Messages sent to the currently authenticated user. + "Get Customer Messages (current)", + """Get messages for the logged in customer + |Messages sent to the currently authenticated user. | |Authentication via OAuth is required.""", emptyObjectJson, @@ -139,8 +140,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "addCustomerMessage", "POST", "/banks/BANK_ID/customer/CUSTOMER_NUMBER/messages", + "Add Customer Message.", "Add a message for the customer specified by CUSTOMER_NUMBER", - "", // We use Extraction.decompose to convert to json Extraction.decompose(AddCustomerMessageJson("message to send", "from department", "from person")), emptyObjectJson, @@ -362,7 +363,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "getTransactionRequestTypes", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types", - "Get Transaction Request Types for an account", + "Get Transaction Request Types for Account", """Returns the Transation Request Types that the account specified by ACCOUNT_ID and view specified by VIEW_ID has access to. | |These are the ways this API Server can create a Transaction via a Transaction Request diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index f8fe39fcc..a7e2aeb66 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -130,8 +130,9 @@ trait APIMethods200 { "allAccountsAllBanks", "GET", "/accounts", - "Get accounts at all banks (Authenticated + Anonymous access).", - """Returns the list of accounts at that the user has access to at all banks. + "Get all Accounts at all Banks.", + """Get all accounts at all banks the User has access to (Authenticated + Anonymous access). + |Returns the list of accounts at that the user has access to at all banks. |For each account the API returns the account ID and the available views. | |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views. If @@ -204,8 +205,9 @@ trait APIMethods200 { "publicAccountsAllBanks", "GET", "/accounts/public", - "Get public accounts at all banks (Anonymous access).", - """Returns the list of accounts containing public views at all banks + "Get Public Accounts at all Banks.", + """Get public accounts at all banks (Anonymous access). + |Returns the list of accounts containing public views at all banks |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", emptyObjectJson, emptyObjectJson, @@ -238,8 +240,9 @@ trait APIMethods200 { "allAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts", - "Get accounts at one bank (Authenticated + Anonymous access).", - """Returns the list of accounts at BANK_ID that the user has access to. + "Get Accounts at one Bank.", + """Get accounts at one bank that the user has access to (Authenticated + Anonymous access). + |Returns the list of accounts at BANK_ID that the user has access to. |For each account the API returns the account ID and the available views. | |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views. @@ -333,8 +336,8 @@ trait APIMethods200 { "publicAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/public", - "Get public accounts at one bank (Anonymous access).", - """Returns a list of the public accounts at BANK_ID. For each account the API returns the ID and the available views. + "Get public Accounts at Bank.", + """Returns a list of the public accounts (Anonymous access) at BANK_ID. For each account the API returns the ID and the available views. | |Authentication via OAuth is not required.""", emptyObjectJson, @@ -364,8 +367,9 @@ trait APIMethods200 { "getKycDocuments", "GET", "/customers/CUSTOMER_NUMBER/kyc_documents", - "Get KYC (know your customer) documents for a customer", - """Get a list of documents that affirm the identity of the customer + "Get KYC Documents for Customer", + """Get KYC (know your customer) documents for a customer + |Get a list of documents that affirm the identity of the customer |Passport, driving licence etc. |Authentication is required.""", emptyObjectJson, @@ -431,8 +435,9 @@ trait APIMethods200 { "getKycChecks", "GET", "/customers/CUSTOMER_NUMBER/kyc_checks", - "Get KYC checks for the logged in customer", - """Messages sent to the currently authenticated user. + "Get KYC Checks for current Customer", + """Get KYC checks for the logged in customer + |Messages sent to the currently authenticated user. | |Authentication via OAuth is required.""", emptyObjectJson, @@ -532,8 +537,8 @@ trait APIMethods200 { "addKycDocument", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_documents", - "Add a KYC document for the customer specified by CUSTOMER_NUMBER", - "KYC Documents contain the document type (e.g. passport), place of issue, expiry etc. ", + "Add KYC Document.", + "Add a KYC document for the customer specified by CUSTOMER_NUMBER. KYC Documents contain the document type (e.g. passport), place of issue, expiry etc. ", Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -577,8 +582,8 @@ trait APIMethods200 { "addKycMedia", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_media", - "Add some KYC media for the customer specified by CUSTOMER_NUMBER", - "KYC Media resources relate to KYC Documents and KYC Checks and contain media urls for scans of passports, utility bills etc.", + "Add KYC Media.", + "Add some KYC media for the customer specified by CUSTOMER_NUMBER. KYC Media resources relate to KYC Documents and KYC Checks and contain media urls for scans of passports, utility bills etc.", Extraction.decompose(KycMediaJSON("73hyfgayt6ywerwerasd", "1239879", "image", "http://www.example.com/id-docs/123/image.png", exampleDate, "wuwjfuha234678", "98FRd987auhf87jab")), emptyObjectJson, emptyObjectJson :: Nil, @@ -620,8 +625,8 @@ trait APIMethods200 { "addKycCheck", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_check", - "Add a KYC check for the customer specified by CUSTOMER_NUMBER", - "KYC Checks store details of checks on a customer made by the KYC team, their comments and a satisfied status.", + "Add KYC Check", + "Add a KYC check for the customer specified by CUSTOMER_NUMBER. KYC Checks store details of checks on a customer made by the KYC team, their comments and a satisfied status.", Extraction.decompose(KycCheckJSON("98FRd987auhf87jab", "1239879", exampleDate, "online_meeting", "67876", "Simon Redfern", true, "")), emptyObjectJson, emptyObjectJson :: Nil, @@ -664,8 +669,8 @@ trait APIMethods200 { "addKycStatus", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_statuses", - "Add a kyc_status for the customer specified by CUSTOMER_NUMBER", - "KYC Status is a timeline of the KYC status of the customer", + "Add KYC Status", + "Add a kyc_status for the customer specified by CUSTOMER_NUMBER. KYC Status is a timeline of the KYC status of the customer", Extraction.decompose(KycStatusJSON("8762893876", true, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -703,8 +708,8 @@ trait APIMethods200 { "addSocialMediaHandle", "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media", - "Add a social media handle for the customer specified by CUSTOMER_NUMBER", - "", + "Add Social Media Handle", + "Add a social media handle for the customer specified by CUSTOMER_NUMBER.", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -1347,8 +1352,10 @@ trait APIMethods200 { "createMeeting", "POST", "/banks/BANK_ID/meetings", - "Create Meeting: Initiate a video conference/call with the bank.", - """The Meetings resource contains meta data about video/other conference sessions, not the video/audio/chat itself. + "Create Meeting (video conference/call)", + """Create Meeting: Initiate a video conference/call with the bank. + | + |The Meetings resource contains meta data about video/other conference sessions, not the video/audio/chat itself. | |The actual conferencing is handled by external providers. Currently OBP supports tokbox video conferences (WIP). | @@ -1760,8 +1767,8 @@ trait APIMethods200 { "deleteEntitlement", "DELETE", "/users/USER_ID/entitlement/ENTITLEMENT_ID", - "Delete Entitlement specified by ENTITLEMENT_ID for an user specified by USER_ID", - """ + "Delete Entitlement", + """Delete Entitlement specified by ENTITLEMENT_ID for an user specified by USER_ID | |Authentication is required and the user needs to be a Super Admin. |Super Admins are listed in the Props file. @@ -1930,9 +1937,9 @@ trait APIMethods200 { "elasticSearchMetrics", "GET", "/search/metrics", - "Search Metrics Data Via Elastic (search)", + "Search API Metrics via Elasticsearch.", """ - |Search metrics data via Elastic Search. + |Search the API calls made to this API instance via Elastic Search. | |Login is required. | diff --git a/src/main/scala/code/views/MapperViews.scala b/src/main/scala/code/views/MapperViews.scala index f6010aad2..30cfcfbcb 100644 --- a/src/main/scala/code/views/MapperViews.scala +++ b/src/main/scala/code/views/MapperViews.scala @@ -124,6 +124,13 @@ private object MapperViews extends Views with Loggable { } else true } + + + + /* + This removes the link between a User and a View (View Privileges) + */ + def revokeAllPermission(bankId : BankId, accountId: AccountId, user : User) : Box[Boolean] = { //TODO: make this more efficient by using one query (with a join) val allUserPrivs = ViewPrivileges.findAll(By(ViewPrivileges.user, user.apiId.value)) From 9bd6ad165078d6fa71fa65ac6c8353a6681c1dc5 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Thu, 30 Jun 2016 13:06:54 +0200 Subject: [PATCH 573/702] Added white text color in tables for black background in socgen CSS --- src/main/webapp/media/css/overrides/socgen.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/media/css/overrides/socgen.css b/src/main/webapp/media/css/overrides/socgen.css index f70eb8213..12d54f7fd 100644 --- a/src/main/webapp/media/css/overrides/socgen.css +++ b/src/main/webapp/media/css/overrides/socgen.css @@ -4,3 +4,7 @@ #header-decoration { background-color: #000000 !important; } + +#registerAppSection table { + color: white; +} From a06f16ae4c1ef287aabd0338cc0236d5af598121 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 30 Jun 2016 13:11:32 +0200 Subject: [PATCH 574/702] Add logging to kafka connector view creation methods --- .../bankconnectors/KafkaMappedConnector.scala | 57 ++++--- .../code/transaction/KafkaTransaction.scala | 158 ------------------ 2 files changed, 34 insertions(+), 181 deletions(-) delete mode 100644 src/main/scala/code/transaction/KafkaTransaction.scala diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index e14368c9f..751fb7e82 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -14,10 +14,9 @@ import code.model._ import code.model.dataAccess._ import code.sandbox.{CreateViewImpls, Saveable} import code.tesobe.CashTransaction -import code.transaction.{KafkaTransaction, MappedTransaction} +import code.transaction.MappedTransaction import code.transactionrequests.MappedTransactionRequest import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} -import code.util.Helper import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge} import code.util.{Helper, TTLCache} import code.views.Views @@ -74,12 +73,15 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable def setAccountOwner(owner : String, account: KafkaInboundAccount) : Unit = { val apiUserOwner = APIUser.findAll.find(user => owner == user.emailAddress) + logger.info(s"------------> list of all apiusers: ${APIUser.findAll}") + logger.info(s"------------> looking for: ${owner}, found: apiUserOwner ${apiUserOwner}") apiUserOwner match { case Some(o) => { - MappedAccountHolder.create + val holder = MappedAccountHolder.create .user(o) .accountBankPermalink(account.bank) - .accountPermalink(account.id).save + .accountPermalink(account.id).saveMe + logger.info(s"------------> created mappeduserholder: ${holder}") } case None => { //This shouldn't happen as OBPUser should generate the APIUsers when saved @@ -105,6 +107,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable views.foreach(_.save()) views.map(_.value).filterNot(_.isPublic).foreach(v => { Views.views.vend.addPermission(v.uid, apiUser) + logger.info(s"------------> added view: ${v.uid} for apiuser: ${apiUser}") }) setAccountOwner(apiUser.email.get, r) } @@ -412,34 +415,37 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable * Saves a transaction with amount @amt and counterparty @counterparty for account @account. Returns the id * of the saved transaction. */ - private def saveTransaction(account : AccountType, counterparty : BankAccount, amt : BigDecimal, description : String) : Box[TransactionId] = { + private def saveTransaction(account : AccountType, + counterparty : BankAccount, + amt : BigDecimal, + description : String) : Box[TransactionId] = { val transactionTime = now val currency = account.currency - //update the balance of the account for which a transaction is being created val newAccountBalance : Long = account.balance.toLong + Helper.convertToSmallestCurrencyUnits(amt, account.currency) //account.balance = newAccountBalance - val mappedTransaction = MappedTransaction.create - .bank(account.bankId.value) - .account(account.accountId.value) - .transactionType("sandbox-payment") - .amount(Helper.convertToSmallestCurrencyUnits(amt, currency)) - .newAccountBalance(newAccountBalance) - .currency(currency) - .tStartDate(transactionTime) - .tFinishDate(transactionTime) - .description(description) - .counterpartyAccountHolder(counterparty.accountHolder) - .counterpartyAccountNumber(counterparty.number) - .counterpartyAccountKind(counterparty.accountType) - .counterpartyBankName(counterparty.bankName) - .counterpartyIban(counterparty.iban.getOrElse("")) - .counterpartyNationalId(counterparty.nationalIdentifier).saveMe + val reqId: String = UUID.randomUUID().toString + // Create argument list with reqId + // in order to fetch corresponding response + val argList = Map("username" -> OBPUser.getCurrentUserUsername, + "accountId" -> account.accountId.value, + "currency" -> currency, + "amount" -> amt.toString, + "otherAccountId" -> counterparty.accountId.value, + "otherAccountCurrency" -> counterparty.currency, + "transactionType" -> "AC") + // Since result is single account, we need only first list entry + implicit val formats = net.liftweb.json.DefaultFormats + val r = process(reqId, "saveTransaction", argList) //.extract[KafkaInboundTransactionId] + + r.extract[KafkaInboundTransactionId] match { + case r: KafkaInboundTransactionId => Full(TransactionId(r.transactionId)) + case _ => Full(TransactionId("0")) + } - Full(mappedTransaction.theTransactionId) } /* @@ -1129,5 +1135,10 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable number : String // customer number, also known as ownerId (owner of accounts) aka API User? ) + + case class KafkaInboundTransactionId( + transactionId : String + ) + } diff --git a/src/main/scala/code/transaction/KafkaTransaction.scala b/src/main/scala/code/transaction/KafkaTransaction.scala deleted file mode 100644 index 677230f21..000000000 --- a/src/main/scala/code/transaction/KafkaTransaction.scala +++ /dev/null @@ -1,158 +0,0 @@ -package code.transaction - -import java.util.UUID - -import code.bankconnectors.Connector -import code.model._ -import net.liftweb.common.Logger - -class KafkaTransaction extends TransactionUUID { - - var _bank = "" - var _account = "" - var _counterpartyIban = "" - - def counterpartyIban: String ={ _counterpartyIban } - - private val logger = Logger(classOf[KafkaTransaction]) - - def bank: String = _bank - def account: String = _account - val transactionId: String = UUID.randomUUID().toString - - - override def theTransactionId = TransactionId(transactionId) - override def theAccountId = AccountId(account) - override def theBankId = BankId(bank) - - def getCounterpartyIban() = { - val i = counterpartyIban - if(i.isEmpty) None else Some(i) - } - - def toTransaction(account: BankAccount): Option[Transaction] = { - /* val tBankId = theBankId - val tAccId = theAccountId - - if (tBankId != account.bankId || tAccId != account.accountId) { - logger.warn("Attempted to convert KafkaTransaction to Transaction using unrelated existing BankAccount object") - None - } else { - val label = { - val d = description.get - if (d.isEmpty) None else Some(d) - } - - val transactionCurrency = currency.get - val amt = Helper.smallestCurrencyUnitToBigDecimal(amount.get, transactionCurrency) - val newBalance = Helper.smallestCurrencyUnitToBigDecimal(newAccountBalance.get, transactionCurrency) - - def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { - new OtherBankAccount( - id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), - label = counterpartyAccountHolder.get, - nationalIdentifier = counterpartyNationalId.get, - swift_bic = None, //TODO: need to add this to the json/model - iban = getCounterpartyIban(), - number = counterpartyAccountNumber.get, - bankName = counterpartyBankName.get, - kind = counterpartyAccountKind.get, - originalPartyBankId = theBankId, - originalPartyAccountId = theAccountId, - alreadyFoundMetadata = alreadyFoundMetadata - ) - } - - //it's a bit confusing what's going on here, as normally metadata should be automatically generated if - //it doesn't exist when an OtherBankAccount object is created. The issue here is that for legacy reasons - //otherAccount ids are metadata ids, so the metadata needs to exist before we created the OtherBankAccount - //so that we know what id to give it. - - //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) - val dummyOtherBankAccount = createOtherBankAccount(None) - - //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object - //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init - val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata)) - - Some(new Transaction( - transactionUUID.get, - theTransactionId, - account, - otherAccount, - transactionType.get, - amt, - transactionCurrency, - label, - tStartDate.get, - tFinishDate.get, - newBalance)) - } - */ - null - } - - def toTransaction : Option[Transaction] = { - for { - acc <- Connector.connector.vend.getBankAccount(theBankId, theAccountId) - transaction <- toTransaction(acc) - } yield transaction - } - -} - -object KafkaTransaction extends KafkaTransaction { - /* - private val logger = Logger(classOf[KafkaTransaction]) - - object bank extends MappedString(this, 255) - object account extends MappedString(this, 255) - object transactionId extends MappedString(this, 255) { - override def defaultValue = UUID.randomUUID().toString - } - //TODO: review the need for this - // (why do we need transactionUUID and transactionId - which is a UUID?) - object transactionUUID extends MappedUUID(this) - object transactionType extends MappedString(this, 100) - - //amount/new balance use the smallest unit of currency! e.g. cents, yen, pence, øre, etc. - object amount extends MappedLong(this) - object newAccountBalance extends MappedLong(this) - - object currency extends MappedString(this, 10) // This should probably be 3 only characters long - - object tStartDate extends MappedDateTime(this) - object tFinishDate extends MappedDateTime(this) - - object description extends DefaultStringField(this) - - object counterpartyAccountNumber extends MappedAccountNumber(this) - object counterpartyAccountHolder extends MappedString(this, 100) - //still unclear exactly how what this is defined to mean - object counterpartyNationalId extends MappedString(this, 40) - //this should eventually be calculated using counterpartyNationalId - object counterpartyBankName extends MappedString(this, 100) - //this should eventually either generate counterpartyAccountNumber or be generated - object counterpartyIban extends MappedString(this, 100) - object counterpartyAccountKind extends MappedString(this, 40) - - //This is a holder for storing data from a previous model version that wasn't set correctly - //e.g. some previous models had counterpartyAccountNumber set to a string that was clearly - //not a valid account number, though the string may have actually contained the account number - //somewhere within it (e.g. "BLS 3020201 BLAH BLAH S/C 2014-05-22") - // - // We save information like this so that we can try to manually process it later. - // - // Keep in mind that changing the counterparty account number will require an update - // to the corresponding counterparty metadata object! - @deprecated - object extraInfo extends DefaultStringField(this) - - - override def theTransactionId = TransactionId(transactionId.get) - override def theAccountId = AccountId(account.get) - override def theBankId = BankId(bank.get) -*/ - - -} From 5c246b882a2e7e70d11dbff0849f97ec95bb021b Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 30 Jun 2016 14:32:46 +0200 Subject: [PATCH 575/702] This closes #70 - Deprecated method get of Lift framework 2.6 --- .../scala/code/api/v2_0_0/APIMethods200.scala | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index a7e2aeb66..8ca6f309d 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -557,7 +557,7 @@ trait APIMethods200 { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycDocumentJSON]} ?~! ErrorMessages.InvalidJsonFormat - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound kycDocumentCreated <- booleanToBox( KycDocuments.kycDocumentProvider.vend.addKycDocuments( @@ -600,7 +600,7 @@ trait APIMethods200 { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycMediaJSON]} ?~! ErrorMessages.InvalidJsonFormat - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound kycDocumentCreated <- booleanToBox( KycMedias.kycMediaProvider.vend.addKycMedias( @@ -643,7 +643,7 @@ trait APIMethods200 { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycCheckJSON]} ?~! ErrorMessages.InvalidJsonFormat - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound kycCheckCreated <- booleanToBox( KycChecks.kycCheckProvider.vend.addKycChecks( @@ -687,7 +687,7 @@ trait APIMethods200 { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycStatusJSON]} ?~! ErrorMessages.InvalidJsonFormat - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound kycStatusCreated <- booleanToBox( KycStatuses.kycStatusProvider.vend.addKycStatus( @@ -726,7 +726,7 @@ trait APIMethods200 { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[SocialMediaJSON]} ?~! ErrorMessages.InvalidJsonFormat - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound kycSocialMediaCreated <- booleanToBox( SocialMediaHandle.socialMediaHandleProvider.vend.addSocialMedias( @@ -879,10 +879,10 @@ trait APIMethods200 { user => for { u <- user ?~! ErrorMessages.UserNotLoggedIn // Check we have a user (rather than error or empty) - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} // Check bank exists. - account <- tryo(BankAccount(bank.bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // Check bank exists. + account <- BankAccount(bank.bankId, accountId) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. availableViews <- Full(account.permittedViews(user)) - view <- tryo(View.fromUrl(viewId, account).get) ?~! {ErrorMessages.ViewNotFound} + view <- View.fromUrl(viewId, account) ?~! {ErrorMessages.ViewNotFound} moderatedAccount <- account.moderatedBankAccount(view, user) } yield { val viewsAvailable = availableViews.map(JSONFactory121.createViewJSON).sortBy(_.short_name) @@ -921,8 +921,8 @@ trait APIMethods200 { user => for { u <- user ?~! ErrorMessages.UserNotLoggedIn // Check we have a user (rather than error or empty) - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} // Check bank exists. - account <- tryo(BankAccount(bank.bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // Check bank exists. + account <- BankAccount(bank.bankId, accountId) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. permissions <- account permissions u } yield { val permissionsJSON = JSONFactory121.createPermissionsJSON(permissions.sortBy(_.user.emailAddress)) @@ -956,8 +956,8 @@ trait APIMethods200 { user => for { u <- user ?~! ErrorMessages.UserNotLoggedIn // Check we have a user (rather than error or empty) - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} // Check bank exists. - account <- tryo(BankAccount(bank.bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // Check bank exists. + account <- BankAccount(bank.bankId, accountId) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. permission <- account permission(u, providerId, userId) } yield { val views = JSONFactory121.createViewsJSON(permission.views.sortBy(_.viewId.value)) @@ -1070,7 +1070,7 @@ trait APIMethods200 { Box(Some(1)) else user ?~! "User must be logged in to retrieve Transaction Types data" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} transactionTypes <- TransactionType.TransactionTypeProvider.vend.getTransactionTypesForBank(bank.bankId) // ~> APIFailure("No transation types available. License may not be set.", 204) } yield { // Format the data as json @@ -1159,12 +1159,12 @@ trait APIMethods200 { u <- user ?~ ErrorMessages.UserNotLoggedIn transBodyJson <- tryo{json.extract[TransactionRequestBodyJSON]} ?~ {ErrorMessages.InvalidJsonFormat} transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)} - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} isOwnerOrHasEntitlement <- booleanToBox(u.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, u.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toBankId <- tryo(BankId(transBodyJson.to.bank_id)) toAccountId <- tryo(AccountId(transBodyJson.to.account_id)) - toAccount <- tryo{BankAccount(toBankId, toAccountId).get} ?~! {ErrorMessages.CounterpartyNotFound} + toAccount <- BankAccount(toBankId, toAccountId) ?~! {ErrorMessages.CounterpartyNotFound} // Prevent default value for transaction request type (at least). // Consider: Add valid list of Transaction Request Types to Props "transactionRequests_supported_types" and use that below isValidTransactionRequestType <- tryo(assert(transactionRequestType.value != "TRANSACTION_REQUEST_TYPE")) ?~! s"${ErrorMessages.InvalidTransactionRequestType} : Invalid value is: '${transactionRequestType.value}' Valid values are: ${TransactionRequests.CHALLENGE_SANDBOX_TAN}" @@ -1203,9 +1203,9 @@ trait APIMethods200 { user => if (Props.getBool("transactionRequests_enabled", false)) { for { - u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {"Unknown bank account"} + u: User <- user ?~ ErrorMessages.UserNotLoggedIn + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {"Unknown bank account"} view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} answerJson <- tryo{json.extract[ChallengeAnswerJSON]} ?~ {"Invalid json format"} //TODO check more things here @@ -1272,8 +1272,8 @@ trait APIMethods200 { if (Props.getBool("transactionRequests_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} transactionRequests <- Connector.connector.vend.getTransactionRequests(u, fromAccount) } @@ -1387,7 +1387,7 @@ trait APIMethods200 { providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) u <- user ?~ ErrorMessages.UserNotLoggedIn - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} postedData <- tryo {json.extract[CreateMeetingJSON]} ?~ ErrorMessages.InvalidJsonFormat now = Calendar.getInstance().getTime() sessionId <- tryo{code.opentok.OpenTokUtil.getSession.getSessionId()} @@ -1436,12 +1436,11 @@ trait APIMethods200 { if (Props.getBool("meeting.tokbox_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - + fromBank <- tryo(Bank(bankId)) ?~! {ErrorMessages.BankNotFound} providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) u <- user ?~ ErrorMessages.UserNotLoggedIn - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // now = Calendar.getInstance().getTime() meetings <- Meeting.meetingProvider.vend.getMeetings(bank.bankId, u) } @@ -1488,12 +1487,12 @@ trait APIMethods200 { if (Props.getBool("meeting.tokbox_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + fromBank: Box[Bank] <- tryo(Bank(bankId)) ?~! {ErrorMessages.BankNotFound} providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) u <- user ?~ ErrorMessages.UserNotLoggedIn - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - meeting <- Meeting.meetingProvider.vend.getMeeting(bank.bankId, u, meetingId) + bank: Bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + meeting <- Meeting.meetingProvider.vend.getMeeting(bank.bankId, u, meetingId) ?~ "gdgd" } yield { // Format the data as V2.0.0 json @@ -1537,7 +1536,7 @@ trait APIMethods200 { user => for { u <- user ?~! "User must be logged in to post Customer" // TODO. CHECK user has role to create a customer / create a customer for another user id. - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} canCreateCustomer <- Entitlement.entitlement.vend.getEntitlement(bank.bankId.value, u.userId, CanCreateCustomer.toString) ?~ {ErrorMessages.UserDoesNotHaveRole + CanCreateCustomer +"."} postedData <- tryo{json.extract[CreateCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists From f8c0b53250bfe94ee6c2533d668b5ecf5e008084 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 30 Jun 2016 15:10:23 +0200 Subject: [PATCH 576/702] Added more logging to kafka connector view creation methods --- .../scala/code/bankconnectors/KafkaMappedConnector.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 751fb7e82..f6ad19b02 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -72,6 +72,10 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def setAccountOwner(owner : String, account: KafkaInboundAccount) : Unit = { + logger.error(s"------------> list of all apiusers: ${APIUser.findAll}") + println(s"------------> list of all apiusers: ${APIUser.findAll}") + logger.error(s"------------> owner=${owner}") + println(s"------------> owner=${owner}") val apiUserOwner = APIUser.findAll.find(user => owner == user.emailAddress) logger.info(s"------------> list of all apiusers: ${APIUser.findAll}") logger.info(s"------------> looking for: ${owner}, found: apiUserOwner ${apiUserOwner}") @@ -91,6 +95,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def updateUserAccountViews( apiUser: APIUser ) = { + logger.error(s"------------> UPDATING VIEWS") + println(s"------------> UPDATING VIEWS") // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId From 7608ea4dcab621cf018f4db19a6c9ad564322a0e Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 30 Jun 2016 22:23:48 +0200 Subject: [PATCH 577/702] Add logging to Kafka getBanks --- .../code/bankconnectors/KafkaMappedConnector.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index f6ad19b02..982e280f7 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -176,20 +176,32 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable override def getBanks: List[Bank] = { // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString + + logger.info(s"Kafka getBanks says reqId is: $reqId") + // Create empty argument list val argList = Map( "username" -> OBPUser.getCurrentUserUsername ) + + logger.debug(s"Kafka getBanks says: argList is: $argList") // Send request to Kafka, marked with reqId // so we can fetch the corresponding response implicit val formats = net.liftweb.json.DefaultFormats + + logger.debug(s"Kafka getBanks before cachedBanks.getOrElseUpdate") val rList = { cachedBanks.getOrElseUpdate( argList.toString, () => process(reqId, "getBanks", argList).extract[List[KafkaInboundBank]]) } + + logger.debug(s"Kafka getBanks says rList is $rList") + // Loop through list of responses and create entry for each val res = { for ( r <- rList ) yield { new KafkaBank(r) } } // Return list of results + + logger.debug(s"Kafka getBanks says res is $res") res } From 5d8fe44728b819a30010db1122a04d65d25a030d Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 30 Jun 2016 22:24:25 +0200 Subject: [PATCH 578/702] Changing summaries. Tweak roles on createCustomer --- .../scala/code/api/v1_2_1/APIMethods121.scala | 36 ++++++++------- .../scala/code/api/v2_0_0/APIMethods200.scala | 46 ++++++++++++------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index dc95dfafc..8284d8eeb 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -120,8 +120,9 @@ trait APIMethods121 { "allBanks", "GET", "/banks", - "Get banks on this API instance", - """Returns a list of banks supported on this server: + "Get Banks", + """Get banks on this API instance + |Returns a list of banks supported on this server: | |* ID used as parameter in URLs |* Short and full name of bank @@ -1185,7 +1186,7 @@ trait APIMethods121 { "deleteCounterpartyPrivateAlias", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", - "Delete private alias of other bank account.", + "Delete Counterparty Private Alias", """Deletes the private alias of the other account OTHER_ACCOUNT_ID. | |OAuth authentication is required if the view is not public.""", @@ -1220,7 +1221,7 @@ trait APIMethods121 { "addCounterpartyMoreInfo", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/more_info", - "Add more info to other bank account.", + "Add Counterparty More Info", "Add a description of the counter party from the perpestive of the account e.g. My dentist.", Extraction.decompose(MoreInfoJSON("More info")), emptyObjectJson, @@ -1255,8 +1256,8 @@ trait APIMethods121 { "updateCounterpartyMoreInfo", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/more_info", - "Update more info of other bank account", - "Update the description of the counter party from the perpestive of the account e.g. My dentist.", + "Update Counterparty More Info", + "Update the more info description of the counter party from the perpestive of the account e.g. My dentist.", Extraction.decompose(MoreInfoJSON("More info")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1497,8 +1498,8 @@ trait APIMethods121 { "deleteCounterpartyImageUrl", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/image_url", + "Delete Counterparty Image URL", "Delete image url of other bank account.", - "", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -1530,8 +1531,8 @@ trait APIMethods121 { "addCounterpartyOpenCorporatesUrl", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/open_corporates_url", - "Add open corporate url to other bank account.", - "", + "Add Open Corporates URL to Counterparty", + "Add open corporates url to other bank account.", Extraction.decompose(OpenCorporateUrlJSON("https://opencorporates.com/companies/gb/04351490")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1565,8 +1566,8 @@ trait APIMethods121 { "updateCounterpartyOpenCorporatesUrl", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/open_corporates_url", + "Update Open Corporates Url of Counterparty", "Update open corporate url of other bank account.", - "", Extraction.decompose(OpenCorporateUrlJSON("https://opencorporates.com/companies/gb/04351490")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1600,8 +1601,8 @@ trait APIMethods121 { "deleteCounterpartyOpenCorporatesUrl", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/open_corporates_url", + "Delete Counterparty Open Corporates URL", "Delete open corporate url of other bank account.", - "", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -1633,7 +1634,7 @@ trait APIMethods121 { "addCounterpartyCorporateLocation", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/corporate_location", - "Add corporate location to other bank account.", + "Add Corporate Location to Counterparty", "Add the geolocation of the counterparty's registered address", Extraction.decompose(CorporateLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, @@ -1707,7 +1708,7 @@ trait APIMethods121 { "deleteCounterpartyCorporateLocation", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/corporate_location", - "Delete Counterparty Metadata Corporate Location.", + "Delete Counterparty Corporate Location.", "Delete corporate location of other bank account. Delete the geolocation of the counterparty's registered address", emptyObjectJson, emptyObjectJson, @@ -1819,7 +1820,7 @@ trait APIMethods121 { "deleteCounterpartyPhysicalLocation", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/physical_location", - "Delete Counterparty Metadata Physical Location.", + "Delete Counterparty Physical Location.", "Delete physical location of other bank account.", emptyObjectJson, emptyObjectJson, @@ -1855,7 +1856,7 @@ trait APIMethods121 { "getTransactionsForBankAccount", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", - "Get transactions.", + "Get Transactions for Account (Full)", """Returns transactions list of the account specified by ACCOUNT_ID and [moderated](#1_2_1-getViewsForBankAccount) by the view (VIEW_ID). | |Authentication via OAuth is required if the view is not public. @@ -2513,8 +2514,9 @@ Authentication via OAuth is required. The user must either have owner privileges "getCounterpartyForTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/other_account", - "Get other account of a transaction.", - """Returns details of the other party involved in the transaction, moderated by the [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). + "Get Counterparty of Transaction", + """Get other account of a transaction. + |Returns details of the other party involved in the transaction, moderated by the [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). Authentication via OAuth is required if the view is not public.""", emptyObjectJson, diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index a7e2aeb66..f0e5413b4 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -162,8 +162,9 @@ trait APIMethods200 { "privateAccountsAllBanks", "GET", "/my/accounts", - "Get private accounts at all banks (Authenticated access).", - """Returns the list of accounts containing private views for the user at all banks. + "Get Accounts at all Banks (Private)", + """Get private accounts at all banks (Authenticated access) + |Returns the list of accounts containing private views for the user at all banks. |For each account the API returns the ID and the available views. | |Authentication via OAuth is required.""", @@ -240,7 +241,7 @@ trait APIMethods200 { "allAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts", - "Get Accounts at one Bank.", + "Get Accounts at one Bank (Public and Private).", """Get accounts at one bank that the user has access to (Authenticated + Anonymous access). |Returns the list of accounts at BANK_ID that the user has access to. |For each account the API returns the account ID and the available views. @@ -275,8 +276,9 @@ trait APIMethods200 { "privateAccountsAtOneBank", "GET", "/my/banks/BANK_ID/accounts", - "Get private accounts at one bank (Authenticated access).", - """Returns the list of accounts containing private views for the user at BANK_ID. + "Get Accounts at Bank (Private)", + """Get private accounts at one bank (Authenticated access). + |Returns the list of accounts containing private views for the user at BANK_ID. |For each account the API returns the ID and the available views. | |Authentication via OAuth is required.""", @@ -336,7 +338,7 @@ trait APIMethods200 { "publicAccountsAtOneBank", "GET", "/banks/BANK_ID/accounts/public", - "Get public Accounts at Bank.", + "Get Accounts at Bank (Public)", """Returns a list of the public accounts (Anonymous access) at BANK_ID. For each account the API returns the ID and the available views. | |Authentication via OAuth is not required.""", @@ -749,8 +751,8 @@ trait APIMethods200 { "coreAccountById", "GET", "/my/banks/BANK_ID/accounts/ACCOUNT_ID/account", - "Get account by id.", - """Information returned about an account specified by ACCOUNT_ID: + "Get Account by Id (Core)", + """Information returned about the account specified by ACCOUNT_ID: | |* Number |* Owners @@ -758,6 +760,8 @@ trait APIMethods200 { |* Balance |* IBAN | + |This call returns the owner view and requires access to that view. + | | |OAuth authentication is required""", emptyObjectJson, @@ -798,8 +802,8 @@ trait APIMethods200 { "getCoreTransactionsForBankAccount", "GET", "/my/banks/BANK_ID/accounts/ACCOUNT_ID/transactions", - "Get transactions.", - """Returns transactions list of the account specified by ACCOUNT_ID. + "Get Transactions for Account (Core)", + """Returns transactions list (Core info) of the account specified by ACCOUNT_ID. | |Authentication is required. | @@ -848,7 +852,7 @@ trait APIMethods200 { "accountById", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account", - "Get account by id.", + "Get Account by Id (Full)", """Information returned about an account specified by ACCOUNT_ID as moderated by the view (VIEW_ID): | |* Number @@ -974,9 +978,15 @@ trait APIMethods200 { "createAccount", "PUT", "/banks/BANK_ID/accounts/NEW_ACCOUNT_ID", - "Create Account at bank specified by BANK_ID with Id specified by NEW_ACCOUNT_ID", - "Note: Type is currently ignored and Amount must be zero. You can update the account label with another call (see updateAccountLabel)", - Extraction.decompose(CreateAccountJSON("An user_id","CURRENT", AmountOfMoneyJSON121("EUR", "0"))), + "Create Account", + """Create Account at bank specified by BANK_ID with Id specified by NEW_ACCOUNT_ID. + | + |The account will be owned by the USER_ID specified. If USER_ID is not specified the accout will be owned by the logged in User. + | + |The logged in User must have CanCreateAccount role. + | + |Note: Type is currently ignored and Amount must be zero. You can update the account label with another call (see updateAccountLabel)""".stripMargin, + Extraction.decompose(CreateAccountJSON("A user_id","CURRENT", AmountOfMoneyJSON121("EUR", "0"))), emptyObjectJson, emptyObjectJson :: Nil, false, @@ -1004,7 +1014,8 @@ trait APIMethods200 { user_id <- tryo (if (jsonBody.user_id.nonEmpty) jsonBody.user_id else loggedInUser.userId) ?~ s"Problem getting user_id" postedOrLoggedInUser <- User.findByUserId(user_id) ?~! ErrorMessages.UserNotFoundById bank <- Bank(bankId) ?~ s"Bank $bankId not found" - hasRoles <- booleanToBox(hasEntitlement(bankId.value, loggedInUser.userId, IsHackathonDeveloper) == true || hasEntitlement(bankId.value, loggedInUser.userId, CanCreateAccount) == true, s"Logged in user must have assigned role $CanCreateAccount or $IsHackathonDeveloper") + // TODO IF posted user_id is null user should be able to create Account even without CanCreateAccount as long as balance is 0 + isAllowed <- booleanToBox(hasEntitlement(bankId.value, loggedInUser.userId, CanCreateAccount) == true, s"Logged in user must have assigned role $CanCreateAccount") initialBalanceAsString <- tryo (jsonBody.balance.amount) ?~ s"Problem getting balance amount" accountType <- tryo(jsonBody.`type`) ?~ s"Problem getting type" initialBalanceAsNumber <- tryo {BigDecimal(initialBalanceAsString)} ?~! ErrorMessages.InvalidInitalBalance @@ -1464,8 +1475,9 @@ trait APIMethods200 { "getMeeting", "GET", "/banks/BANK_ID/meetings/MEETING_ID", - "Get Meeting specified by BANK_ID / MEETING_ID", - """Meetings contain meta data about, and are used to facilitate, video conferences / chats etc. + "Get Meeting", + """Get Meeting specified by BANK_ID / MEETING_ID + |Meetings contain meta data about, and are used to facilitate, video conferences / chats etc. | |The actual conference/chats are handled by external services. | From 9892a853023057f36f4cd1d62d3b9593f5f9424b Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 1 Jul 2016 12:03:50 +0200 Subject: [PATCH 579/702] Deprecated method get of Lift framework 2.6 #70 --- .../scala/code/api/v1_4_0/APIMethods140.scala | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 1ad0c47fd..875efcef7 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -88,7 +88,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} info <- Customer.customerProvider.vend.getCustomer(bankId, u) ?~ "No customer information found for current user" } yield { val json = JSONFactory1_4_0.createCustomerJson(info) @@ -122,7 +122,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} //au <- APIUser.find(By(APIUser.id, u.apiId)) //role <- au.isCustomerMessageAdmin ~> APIFailure("User does not have sufficient permissions", 401) } yield { @@ -157,7 +157,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => { for { postedData <- tryo{json.extract[AddCustomerMessageJson]} ?~! "Incorrect json format" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- Customer.customerProvider.vend.getUser(bankId, customerNumber) ?~! "Customer not found" messageCreated <- booleanToBox( CustomerMessages.customerMessageProvider.vend.addMessage( @@ -205,7 +205,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ Box(Some(1)) else user ?~! "User must be logged in to retrieve Branches data" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // Get branches from the active provider branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branches available. License may not be set.", 204) } yield { @@ -253,7 +253,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ Box(Some(1)) else user ?~! "User must be logged in to retrieve ATM data" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} atms <- Box(Atms.atmsProvider.vend.getAtms(bankId)) ~> APIFailure("No ATMs available. License may not be set.", 204) } yield { // Format the data as json @@ -306,7 +306,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ Box(Some(1)) else user ?~! "User must be logged in to retrieve Products data" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} products <- Box(Products.productsProvider.vend.getProducts(bankId)) ~> APIFailure("No products available. License may not be set.", 204) } yield { // Format the data as json @@ -342,7 +342,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ for { // Get crm events from the active provider u <- user ?~! "User must be logged in to retrieve CRM Event information" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} crmEvents <- Box(CrmEvent.crmEventProvider.vend.getCrmEvents(bankId)) ~> APIFailure("No CRM Events available.", 204) } yield { // Format the data as json @@ -395,8 +395,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ if (Props.getBool("transactionRequests_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} transactionRequestTypes <- Connector.connector.vend.getTransactionRequestTypes(u, fromAccount) } yield { @@ -431,8 +431,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ if (Props.getBool("transactionRequests_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} transactionRequests <- Connector.connector.vend.getTransactionRequests(u, fromAccount) } @@ -492,11 +492,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ u <- user ?~ ErrorMessages.UserNotLoggedIn transBodyJson <- tryo{json.extract[TransactionRequestBodyJSON]} ?~ {ErrorMessages.InvalidJsonFormat} transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)} - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {ErrorMessages.AccountNotFound} toBankId <- tryo(BankId(transBodyJson.to.bank_id)) toAccountId <- tryo(AccountId(transBodyJson.to.account_id)) - toAccount <- tryo{BankAccount(toBankId, toAccountId).get} ?~! {ErrorMessages.CounterpartyNotFound} + toAccount <- BankAccount(toBankId, toAccountId) ?~! {ErrorMessages.CounterpartyNotFound} accountsCurrencyEqual <- tryo(assert(fromAccount.currency == toAccount.currency)) ?~! {"Counterparty and holder accounts have differing currencies."} transferCurrencyEqual <- tryo(assert(transBodyJson.value.currency == fromAccount.currency)) ?~! {"Request currency and holder account currency can't be different."} rawAmt <- tryo {BigDecimal(transBodyJson.value.amount)} ?~! s"Amount ${transBodyJson.value.amount} not convertible to number" @@ -536,8 +536,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ if (Props.getBool("transactionRequests_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} - fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {"Unknown bank account"} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + fromAccount <- BankAccount(bankId, accountId) ?~! {"Unknown bank account"} view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} answerJson <- tryo{json.extract[ChallengeAnswerJSON]} ?~ {"Invalid json format"} //TODO check more things here @@ -588,7 +588,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ user => for { u <- user ?~! "User must be logged in to post Customer" - bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, u).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser postedData <- tryo{json.extract[PostCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists From 9de2010f610471b823bf491cbfd50a07fd4c7b66 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 1 Jul 2016 14:20:13 +0200 Subject: [PATCH 580/702] Get rid of warning - method mapToType in object MappedField is deprecated: Automatic conversion to the field's type is not safe and will be removed. Please use field.get instead --- src/main/scala/code/meetings/MappedMeetingProvider.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/meetings/MappedMeetingProvider.scala b/src/main/scala/code/meetings/MappedMeetingProvider.scala index 4e79c1262..14414a419 100644 --- a/src/main/scala/code/meetings/MappedMeetingProvider.scala +++ b/src/main/scala/code/meetings/MappedMeetingProvider.scala @@ -88,7 +88,7 @@ class MappedMeeting extends Meeting with LongKeyedMapper[MappedMeeting] with IdP override def purposeId : String = mPurposeId.get override def bankId : String = mBankId.get.toString - override def keys = MeetingKeys(mSessionId, mCustomerToken, mStaffToken) + override def keys = MeetingKeys(mSessionId.get, mCustomerToken.get, mStaffToken.get) override def present = MeetingPresent(staffUserId = mStaffUserId.get.toString, customerUserId = mCustomerUserId.get.toString) From 2acd51be7d15c73ded6fec7dd1fcccf608e3cf8f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 3 Jul 2016 01:52:06 +0200 Subject: [PATCH 581/702] Adding Direct Login info and link to consumer-registration --- .../code/snippet/ConsumerRegistration.scala | 5 ++- src/main/webapp/consumer-registration.html | 32 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/snippet/ConsumerRegistration.scala b/src/main/scala/code/snippet/ConsumerRegistration.scala index 4396041d5..9c8cb0e00 100644 --- a/src/main/scala/code/snippet/ConsumerRegistration.scala +++ b/src/main/scala/code/snippet/ConsumerRegistration.scala @@ -85,6 +85,7 @@ class ConsumerRegistration extends Loggable { def showResults(consumer : Consumer) = { val urlOAuthEndpoint = Props.get("hostname", "") + "/oauth/initiate" + val urlDirectLoginEndpoint = Props.get("hostname", "") + "/my/logins/direct" //thanks for registering, here's your key, etc. ".app-name *" #> consumer.name.get & ".app-user-authentication-url *" #> consumer.userAuthenticationURL & @@ -95,7 +96,9 @@ class ConsumerRegistration extends Loggable { ".secret-key *" #> consumer.secret.get & ".registration" #> "" & ".oauth-endpoint a *" #> urlOAuthEndpoint & - ".oauth-endpoint a [href]" #> urlOAuthEndpoint + ".oauth-endpoint a [href]" #> urlOAuthEndpoint & + ".directlogin-endpoint a *" #> urlDirectLoginEndpoint & + ".directlogin-endpoint a [href]" #> urlDirectLoginEndpoint } def saveAndShowResults(consumer : Consumer) = { diff --git a/src/main/webapp/consumer-registration.html b/src/main/webapp/consumer-registration.html index 63ce7b124..a0ae6a039 100644 --- a/src/main/webapp/consumer-registration.html +++ b/src/main/webapp/consumer-registration.html @@ -145,9 +145,39 @@ Berlin 13359, Germany endpoint
    - How to use OAuth for OpenBankProject + + + + OAuth Documentation + + + How to use OAuth for OpenBankProject + + + + + + + + Direct Login Endpoint + + + endpoint
    + + + + + + Direct Login Documentation + + + How to use Direct Login + + + + From 808c0c1d38af7133eae08c736ef3eae4d06977e3 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 3 Jul 2016 13:38:14 +0200 Subject: [PATCH 582/702] Layout of MappedCustomerProvider createCustomer --- .../customer/MappedCustomerProvider.scala | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 94775e036..8407d261e 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -55,14 +55,22 @@ object MappedCustomerProvider extends CustomerProvider { lastOkDate: Date) : Box[Customer] = { val createdCustomer = MappedCustomer.create - .mBank(bankId.value).mEmail(email).mFaceImageTime(faceImage.date) - .mFaceImageUrl(faceImage.url).mLegalName(legalName) - .mMobileNumber(mobileNumber).mNumber(number).mUser(user.apiId.value) - .mDateOfBirth(dateOfBirth).mRelationshipStatus(relationshipStatus) + .mBank(bankId.value) + .mEmail(email) + .mFaceImageTime(faceImage.date) + .mFaceImageUrl(faceImage.url) + .mLegalName(legalName) + .mMobileNumber(mobileNumber) + .mNumber(number) + .mUser(user.apiId.value) + .mDateOfBirth(dateOfBirth) + .mRelationshipStatus(relationshipStatus) .mDependents(dependents) .mHighestEducationAttained(highestEducationAttained) - .mEmploymentStatus(employmentStatus).mKycStatus(kycStatus) - .mLastOkDate(lastOkDate).saveMe() + .mEmploymentStatus(employmentStatus) + .mKycStatus(kycStatus) + .mLastOkDate(lastOkDate) + .saveMe() Some(createdCustomer) } From 329ed1e4616c6986ae1f20ce9003a136163bf502 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 3 Jul 2016 13:41:55 +0200 Subject: [PATCH 583/702] v2.0.0 createAccount if have role or for logged in user. Fixing Get User (no bank id in check) Tweaks to and new ErrorMessages --- src/main/scala/code/api/util/APIUtil.scala | 19 ++++++++-- .../scala/code/api/v2_0_0/APIMethods200.scala | 37 +++++++++++-------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 165160458..b6a2abc47 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -61,9 +61,6 @@ object ErrorMessages { // General messages val InvalidJsonFormat = "OBP-10001: Incorrect json format." val InvalidNumber = "OBP-10002: Invalid Number. Could not convert value to a number." - val InvalidInitalBalance = "OBP-10003: Invalid Number. Initial balance must be a number, e.g 1000.00" - - // Authentication / Authorisation / User messages val UserNotLoggedIn = "OBP-20001: User not logged in. Authentication is required!" @@ -97,10 +94,26 @@ object ErrorMessages { val MeetingApiKeyNotConfigured = "OBP-30102: Meeting provider API Key is not configured." val MeetingApiSecretNotConfigured = "OBP-30103: Meeting provider Secret is not configured." + + val InvalidAccountInitalBalance = "OBP-30104: Invalid Number. Initial balance must be a number, e.g 1000.00" + val InvalidAccountBalanceCurrency = "OBP-30105: Invalid Balance Currency." + val InvalidAccountBalanceAmount = "OBP-30106: Invalid Balance Amount." + + val InvalidUserId = "OBP-30107: Invalid User Id." + val InvalidAccountType = "OBP-30108: Invalid Account Type." + val InitialBalanceMustBeZero = "OBP-30109: Initial Balance of Account must be Zero (0)." + + + // Transaction related messages: val InvalidTransactionRequestType = "OBP-40001: Invalid value for TRANSACTION_REQUEST_TYPE" val InsufficientAuthorisationToCreateTransactionRequest = "OBP-40002: Insufficient authorisation to create TransactionRequest. The Transaction Request could not be created because you don't have access to the owner view of the from account and you don't have access to canCreateAnyTransactionRequest." + + + + + } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 0eca180b7..ad4cc42aa 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -981,11 +981,12 @@ trait APIMethods200 { "Create Account", """Create Account at bank specified by BANK_ID with Id specified by NEW_ACCOUNT_ID. | - |The account will be owned by the USER_ID specified. If USER_ID is not specified the accout will be owned by the logged in User. | - |The logged in User must have CanCreateAccount role. + |The User can create an Account for themself or an Account for another User if they have CanCreateAccount role. | - |Note: Type is currently ignored and Amount must be zero. You can update the account label with another call (see updateAccountLabel)""".stripMargin, + |If USER_ID is not specified the account will be owned by the logged in User. + | + |Note: Type is currently ignored and the Amount must be zero. You can update the account label with another call (see updateAccountLabel)""".stripMargin, Extraction.decompose(CreateAccountJSON("A user_id","CURRENT", AmountOfMoneyJSON121("EUR", "0"))), emptyObjectJson, emptyObjectJson :: Nil, @@ -1011,16 +1012,16 @@ trait APIMethods200 { for { loggedInUser <- user ?~! ErrorMessages.UserNotLoggedIn jsonBody <- tryo (json.extract[CreateAccountJSON]) ?~ ErrorMessages.InvalidJsonFormat - user_id <- tryo (if (jsonBody.user_id.nonEmpty) jsonBody.user_id else loggedInUser.userId) ?~ s"Problem getting user_id" + user_id <- tryo (if (jsonBody.user_id.nonEmpty) jsonBody.user_id else loggedInUser.userId) ?~ ErrorMessages.InvalidUserId postedOrLoggedInUser <- User.findByUserId(user_id) ?~! ErrorMessages.UserNotFoundById bank <- Bank(bankId) ?~ s"Bank $bankId not found" - // TODO IF posted user_id is null user should be able to create Account even without CanCreateAccount as long as balance is 0 - isAllowed <- booleanToBox(hasEntitlement(bankId.value, loggedInUser.userId, CanCreateAccount) == true, s"Logged in user must have assigned role $CanCreateAccount") - initialBalanceAsString <- tryo (jsonBody.balance.amount) ?~ s"Problem getting balance amount" - accountType <- tryo(jsonBody.`type`) ?~ s"Problem getting type" - initialBalanceAsNumber <- tryo {BigDecimal(initialBalanceAsString)} ?~! ErrorMessages.InvalidInitalBalance + // User can create account for self or an account for another user if they have CanCreateAccount role + isAllowed <- booleanToBox(hasEntitlement(bankId.value, loggedInUser.userId, CanCreateAccount) == true || (user_id == loggedInUser.userId) , s"User must either create account for self or have role $CanCreateAccount") + initialBalanceAsString <- tryo (jsonBody.balance.amount) ?~ ErrorMessages.InvalidAccountBalanceAmount + accountType <- tryo(jsonBody.`type`) ?~ ErrorMessages.InvalidAccountType + initialBalanceAsNumber <- tryo {BigDecimal(initialBalanceAsString)} ?~! ErrorMessages.InvalidAccountInitalBalance isTrue <- booleanToBox(0 == initialBalanceAsNumber) ?~ s"Initial balance must be zero" - currency <- tryo (jsonBody.balance.currency) ?~ s"Problem getting balance currency" + currency <- tryo (jsonBody.balance.currency) ?~ ErrorMessages.InvalidAccountBalanceCurrency // TODO Since this is a PUT, we should replace the resource if it already exists but will need to check persmissions accountDoesNotExist <- booleanToBox(BankAccount(bankId, accountId).isEmpty, s"Account with id $accountId already exists at bank $bankId") @@ -1585,7 +1586,7 @@ trait APIMethods200 { "getCurrentUser", // TODO can we get this string from the val two lines above? "GET", "/users/current", - "Get Current User", + "Get User (Current)", """Get the logged in user | |Login is required. @@ -1641,8 +1642,8 @@ trait APIMethods200 { user => for { l <- user ?~ ErrorMessages.UserNotLoggedIn - b <- tryo{Bank.all.headOption} ?~! {ErrorMessages.BankNotFound} //TODO: This is a temp workaround - canGetAnyUser <- booleanToBox(hasEntitlement(b.get.bankId.value, l.userId, ApiRole.CanGetAnyUser), "CanGetAnyUser entitlement required") + //b <- tryo{Bank.all.headOption} ?~! {ErrorMessages.BankNotFound} //TODO: This is a temp workaround + canGetAnyUser <- booleanToBox(hasEntitlement("", l.userId, ApiRole.CanGetAnyUser), "CanGetAnyUser entitlement required") // Workaround to get userEmail address directly from URI without needing to URL-encode it u <- OBPUser.getApiUserByEmail(CurrentReq.value.uri.split("/").last) ?~! {ErrorMessages.UserNotFoundByEmail} } @@ -1700,10 +1701,14 @@ trait APIMethods200 { "addEntitlement", "POST", "/users/USER_ID/entitlements", - "Add Entitlement to a user.", + "Add Entitlement for a User.", """Create Entitlement. Grant Role to User. | - |Entitlements are used to grant system or bank level roles to users. (For account level privileges, see Views) + |Entitlements are used to grant System or Bank level roles to Users. (For Account level privileges, see Views) + | + |For a System level Role (.e.g CanGetAnyUser), set bank_id to an empty string i.e. "bank_id":"" + | + |For a Bank level Role (e.g. CanCreateAccount), set bank_id to a valid value e.g. "bank_id":"my-bank-id" | |Authentication is required and the user needs to be a Super Admin. Super Admins are listed in the Props file.""", Extraction.decompose(CreateEntitlementJSON("obp-bank-x-gh", "CanQueryOtherUser")), @@ -1712,7 +1717,7 @@ trait APIMethods200 { false, false, false, - List()) + List(apiTagUser)) lazy val addEntitlement : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a list of views From dae0a0f5fc7a0fdff8bdcebf4b114d1d9dfb8ca2 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 3 Jul 2016 13:42:21 +0200 Subject: [PATCH 584/702] Post Customer / Post Counterparty config changes --- src/test/scala/code/sandbox/PostCounterpartyMetadata.scala | 6 +++--- src/test/scala/code/sandbox/PostCustomer.scala | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala b/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala index 5f6b3715d..678946525 100644 --- a/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala +++ b/src/test/scala/code/sandbox/PostCounterpartyMetadata.scala @@ -74,7 +74,7 @@ object PostCounterpartyMetadata extends SendServerRequests { implicit val formats = DefaultFormats //load json for counterpaties - val counterpartyDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/Atmira/atmira_load_02/OBP_sandbox_counterparties_pretty.json" + val counterpartyDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/unicredit_to_load_04/OBP_sandbox_counterparties_pretty.json" // This contains a list of counterparty lists. one list for each region val counerpartyListData = JsonParser.parse(Source.fromFile(counterpartyDataPath) mkString) @@ -110,7 +110,7 @@ object PostCounterpartyMetadata extends SendServerRequests { //load sandbox users from json - val mainDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/Atmira/atmira_load_02/OBP_sandbox_pretty.json" + val mainDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/unicredit_to_load_04/OBP_sandbox_pretty.json" val mainData = JsonParser.parse(Source.fromFile(mainDataPath) mkString) val users = (mainData \ "users").children @@ -164,7 +164,7 @@ object PostCounterpartyMetadata extends SendServerRequests { // Split by dot (.) except split can take a reg expression so must escape the . val bits = bankId.split("\\.") - val region = bits(3) // Use the counterpartyCode from the bankId + val region = bits(2) // Use the counterpartyCode from the bankId println(s"region is ${region}") diff --git a/src/test/scala/code/sandbox/PostCustomer.scala b/src/test/scala/code/sandbox/PostCustomer.scala index a489442a3..3d44616e7 100644 --- a/src/test/scala/code/sandbox/PostCustomer.scala +++ b/src/test/scala/code/sandbox/PostCustomer.scala @@ -101,7 +101,7 @@ object PostCustomer extends SendServerRequests { //load json for customers - val customerDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/ENBD/load_019/OBP_sandbox_customers_pretty.json" + val customerDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/unicredit_to_load_04/OBP_sandbox_customers_pretty.json" // This contains a list of customers. val customerListData = JsonParser.parse(Source.fromFile(customerDataPath) mkString) @@ -122,7 +122,7 @@ object PostCustomer extends SendServerRequests { //load sandbox users from json - val mainDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/ENBD/load_019/OBP_sandbox_pretty.json" + val mainDataPath = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/unicredit_to_load_04/OBP_sandbox_pretty.json" val mainData = JsonParser.parse(Source.fromFile(mainDataPath) mkString) val users = (mainData \ "users").children @@ -184,7 +184,7 @@ object PostCustomer extends SendServerRequests { // For now, create a customer for (b <- banks) { - val url = s"/v2.0.0/banks/${b.id}/customer" + val url = s"/v2.0.0/banks/${b.id}/customers" val result = ObpPost(url, json) if (!result.isEmpty) { println("saved " + c.customer_number + " as customer " + result) From a0c9a3a089c33488d127ecc97d10eeb3d0ef5c58 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 3 Jul 2016 13:43:03 +0200 Subject: [PATCH 585/702] Resource Doc and apiTag tweaks --- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 2 +- src/main/scala/code/api/v1_4_0/APIMethods140.scala | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 8284d8eeb..2da836de4 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -743,7 +743,7 @@ trait APIMethods121 { false, false, false, - List(apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired)) + List(apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired)) lazy val addPermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { //add access for specific user to a specific view diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 875efcef7..da84316a2 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -179,7 +179,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "getBranches", "GET", "/banks/BANK_ID/branches", - "Get branches for the bank", + "Get Bank Branches", s"""Returns information about branches for a single bank specified by BANK_ID including: | |* Name @@ -226,7 +226,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "getAtms", "GET", "/banks/BANK_ID/atms", - "Get ATMS for the bank", + "Get Bank ATMS", s"""Returns information about ATMs for a single bank specified by BANK_ID including: | |* Address @@ -275,8 +275,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "getProducts", "GET", "/banks/BANK_ID/products", - "Get products offered by the bank", - s"""Returns information about financial products offered by a bank specified by BANK_ID including: + "Get Bank Products", + s"""Returns information about the financial products offered by a bank specified by BANK_ID including: | |* Name |* Code From e4bac9de9334ca99360c12865927ffb4a207d9da Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 4 Jul 2016 10:51:21 +0200 Subject: [PATCH 586/702] Tweaking Resource Docs --- .../scala/code/api/v1_2_1/APIMethods121.scala | 24 +++++++++---------- .../scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 2da836de4..bc2406a0c 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -432,8 +432,8 @@ trait APIMethods121 { "updateAccountLabel", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID", - "Change account label.", - "", + "Update Account Label.", + "Update the label for the account. The label is how the account is known to the account owner e.g. 'My savings account' ", Extraction.decompose(UpdateAccountJSON("ACCOUNT_ID of the account we want to update", "New label", "BANK_ID")), emptyObjectJson, emptyObjectJson :: Nil, @@ -599,8 +599,8 @@ trait APIMethods121 { "deleteViewForBankAccount", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID", - "Deletes a view on an bank account.", - "", + "Delete View", + "Deletes the view specified by VIEW_ID on the bank account specified by ACCOUNT_ID at bank BANK_ID.", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -1044,7 +1044,7 @@ trait APIMethods121 { "deleteCounterpartyPublicAlias", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias", - "Delete public alias of other bank account.", + "Delete Counterparty Public Alias", """Deletes the public alias of the other account OTHER_ACCOUNT_ID. | |OAuth authentication is required if the view is not public.""", @@ -1077,7 +1077,7 @@ trait APIMethods121 { "getCounterpartyPrivateAlias", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", - "Get private alias of other bank account.", + "Get Counterparty Private Alias", """Returns the private alias of the other account OTHER_ACCOUNT_ID. | |OAuth authentication is required if the view is not public.""", @@ -1112,7 +1112,7 @@ trait APIMethods121 { "addCounterpartyPrivateAlias", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", - "Add private alias to other bank account.", + "Create Counterparty Private Alias", """Creates a private alias for the other account OTHER_ACCOUNT_ID. | |OAuth authentication is required if the view is not public.""", @@ -1149,8 +1149,8 @@ trait APIMethods121 { "updateCounterpartyPrivateAlias", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", - "Update private alias of other bank account.", - """Updates the private alias of the other account OTHER_ACCOUNT_ID. + "Update Counterparty Private Alias", + """Updates the private alias of the counterparty (AKA other account) OTHER_ACCOUNT_ID. | |OAuth authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), @@ -1463,7 +1463,7 @@ trait APIMethods121 { "updateCounterpartyImageUrl", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/image_url", - "Update image url of other bank account.", + "Update Counterparty Image Url", "Update the url that points to the logo of the counterparty", Extraction.decompose(ImageUrlJSON("www.example.com/logo.png")), emptyObjectJson, @@ -1671,7 +1671,7 @@ trait APIMethods121 { "updateCounterpartyCorporateLocation", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/corporate_location", - "Update corporate location of other bank account.", + "Update Counterparty Corporate Location", "Update the geolocation of the counterparty's registered address", Extraction.decompose(CorporateLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, @@ -1783,7 +1783,7 @@ trait APIMethods121 { "updateCounterpartyPhysicalLocation", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/physical_location", - "Update counterparties physical location", + "Update Counterparty Physical Location", "Update geocoordinates of the counterparty's main location", Extraction.decompose(PhysicalLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index ad4cc42aa..cd1e57b3b 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -986,7 +986,7 @@ trait APIMethods200 { | |If USER_ID is not specified the account will be owned by the logged in User. | - |Note: Type is currently ignored and the Amount must be zero. You can update the account label with another call (see updateAccountLabel)""".stripMargin, + |Note: Type is currently ignored and the Amount must be zero. You can update the account label with another call (see 1_2_1-updateAccountLabel)""".stripMargin, Extraction.decompose(CreateAccountJSON("A user_id","CURRENT", AmountOfMoneyJSON121("EUR", "0"))), emptyObjectJson, emptyObjectJson :: Nil, From 74f68773455d5a94cea4afc9f6cbb137060b526c Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 4 Jul 2016 16:13:48 +0200 Subject: [PATCH 587/702] Tweaking getMeeting end point --- src/main/scala/code/api/util/APIUtil.scala | 1 + src/main/scala/code/api/v2_0_0/APIMethods200.scala | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index b6a2abc47..7d6458575 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -93,6 +93,7 @@ object ErrorMessages { val MeetingsNotSupported = "OBP-30101: Meetings are not supported on this server." val MeetingApiKeyNotConfigured = "OBP-30102: Meeting provider API Key is not configured." val MeetingApiSecretNotConfigured = "OBP-30103: Meeting provider Secret is not configured." + val MeetingNotFound = "OBP-30104: Meeting not found." val InvalidAccountInitalBalance = "OBP-30104: Invalid Number. Initial balance must be a number, e.g 1000.00" diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index cd1e57b3b..9aeb5bf7d 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1448,7 +1448,7 @@ trait APIMethods200 { if (Props.getBool("meeting.tokbox_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank <- tryo(Bank(bankId)) ?~! {ErrorMessages.BankNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) u <- user ?~ ErrorMessages.UserNotLoggedIn @@ -1500,12 +1500,12 @@ trait APIMethods200 { if (Props.getBool("meeting.tokbox_enabled", false)) { for { u <- user ?~ ErrorMessages.UserNotLoggedIn - fromBank: Box[Bank] <- tryo(Bank(bankId)) ?~! {ErrorMessages.BankNotFound} + fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) u <- user ?~ ErrorMessages.UserNotLoggedIn - bank: Bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - meeting <- Meeting.meetingProvider.vend.getMeeting(bank.bankId, u, meetingId) ?~ "gdgd" + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + meeting <- Meeting.meetingProvider.vend.getMeeting(bank.bankId, u, meetingId) ?~! {ErrorMessages.MeetingNotFound} } yield { // Format the data as V2.0.0 json From 7a65e2d30a8cd735ece04e7845da0cacc3d7d99c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Mon, 4 Jul 2016 21:50:24 +0200 Subject: [PATCH 588/702] Removing PSD from meetings calls --- .../scala/code/api/v2_0_0/APIMethods200.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index cd1e57b3b..0a8e3f5b0 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1384,9 +1384,9 @@ trait APIMethods200 { Extraction.decompose(CreateMeetingJSON("tokbox", "onboarding")), emptyObjectJson, emptyObjectJson :: Nil, - true, - true, - true, + false, // Core + false, // PSD2 + false, // OBWG List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) @@ -1436,9 +1436,9 @@ trait APIMethods200 { emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, - true, - true, - true, + false, + false, // PSD2 + false, List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) @@ -1488,9 +1488,9 @@ trait APIMethods200 { emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, - true, - true, - true, + false, + false, + false, List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental)) From 318a3e6e3646aadca1f55d6c64dcc4e3e3284036 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Tue, 5 Jul 2016 08:52:47 +0200 Subject: [PATCH 589/702] This closes #77 - v2.0.0 createCustomer rules --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 797eb804e..28bf8a387 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1552,9 +1552,11 @@ trait APIMethods200 { bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} canCreateCustomer <- Entitlement.entitlement.vend.getEntitlement(bank.bankId.value, u.userId, CanCreateCustomer.toString) ?~ {ErrorMessages.UserDoesNotHaveRole + CanCreateCustomer +"."} postedData <- tryo{json.extract[CreateCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat + isLoggedUser <- booleanToBox(postedData.user_id.nonEmpty == false || postedData.user_id.equalsIgnoreCase(u.userId)) ?~ "User can create a customer for themself only" checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists // TODO The user id we expose should be a uuid . For now we have a long direct from the database. - customer_user <- User.findByUserId(postedData.user_id) ?~! ErrorMessages.UserNotFoundById + user_id <- tryo (if (postedData.user_id.nonEmpty) postedData.user_id else u.userId) ?~ s"Problem getting user_id" + customer_user <- User.findByUserId(user_id) ?~! ErrorMessages.UserNotFoundById customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, customer_user).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser customer <- Customer.customerProvider.vend.addCustomer(bankId, customer_user, From d056144ad7846b846f8b58924b0f2a3367efcf3f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 5 Jul 2016 14:36:17 +0200 Subject: [PATCH 590/702] Hacky changes to KafkaMappedConnector --- .../bankconnectors/KafkaMappedConnector.scala | 30 +++++++++++++++++-- .../scala/code/model/dataAccess/OBPUser.scala | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 982e280f7..0f416aab6 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -95,7 +95,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def updateUserAccountViews( apiUser: APIUser ) = { - logger.error(s"------------> UPDATING VIEWS") + logger.info(s"------------> UPDATING VIEWS") println(s"------------> UPDATING VIEWS") // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString @@ -107,8 +107,24 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val rList = { cachedUserAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) } + + + logger.info(s"------------> userAccounts from Kafka: " + rList) + + + + for (r <- rList) { + logger.info("------------>view " + r + "exists=" + viewExists(r)) + } + + + + + val res = { - for (r <- rList if ! viewExists(r)) yield { + // TODO CHECK + // for (r <- rList if ! viewExists(r)) yield { + for (r <- rList) yield { val views = createSaveableViews(r) views.foreach(_.save()) views.map(_.value).filterNot(_.isPublic).foreach(v => { @@ -141,9 +157,10 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable res } + // TODO check me ! (no view name) def viewExists(acc: KafkaInboundAccount): Boolean = { val res = ViewImpl.findAll.filter { v => - if (v.bankPermalink.get == acc.bank && v.accountPermalink.get == acc.id ) + if (v.bankPermalink.get == acc.bank && v.accountPermalink.get == acc.id) true else false @@ -152,11 +169,15 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def createSaveableViews(acc : KafkaInboundAccount) : List[Saveable[ViewType]] = { + + logger.info(s"Kafka createSaveableViews acc is $acc") val bankId = BankId(acc.bank) val accountId = AccountId(acc.id) val ownerView = createSaveableOwnerView(bankId, accountId) + logger.info(s"Kafka createSaveableViews ownerView is $ownerView") + val publicView = if(acc.generate_public_view) Some(createSaveablePublicView(bankId, accountId)) else None @@ -169,6 +190,9 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable if(acc.generate_auditors_view) Some(createSaveableAuditorsView(bankId, accountId)) else None + + + List(Some(ownerView), publicView, accountantsView, auditorsView).flatten } diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index a032b7b3e..317104a42 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -389,7 +389,7 @@ import net.liftweb.util.Helpers._ if (!extUser.isEmpty) { val u = APIUser.find(By(APIUser.email, extUser.getOrElse(null).email)).getOrElse(null) if (u != null) { - KafkaMappedConnector.updatePublicAccountViews(u) + //KafkaMappedConnector.updatePublicAccountViews(u) KafkaMappedConnector.updateUserAccountViews(u) } From 114dc7db77db07fe449dffc4cd10e1b0c1585782 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 5 Jul 2016 14:36:52 +0200 Subject: [PATCH 591/702] Removing duplicate guard in getMeeting --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 28bf8a387..acffcb6c6 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1503,7 +1503,6 @@ trait APIMethods200 { fromBank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} providerApiKey <- Props.get("meeting.tokbox_api_key") ~> APIFailure(ErrorMessages.MeetingApiKeyNotConfigured, 403) providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403) - u <- user ?~ ErrorMessages.UserNotLoggedIn bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} meeting <- Meeting.meetingProvider.vend.getMeeting(bank.bankId, u, meetingId) ?~! {ErrorMessages.MeetingNotFound} } From 0567eb9b4bfada7c13f0bcf6f03511dc26cd1aaf Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 5 Jul 2016 15:18:25 +0200 Subject: [PATCH 592/702] Using number for label --- src/main/scala/code/bankconnectors/KafkaMappedConnector.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 0f416aab6..de8369c03 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -995,7 +995,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable def balance : BigDecimal = BigDecimal(r.balance.amount) def currency : String = r.balance.currency def name : String = r.owners.head - def label : String = r.label + def label : String = r.number def swift_bic : Option[String] = Some("swift_bic") //TODO def iban : Option[String] = Some(r.IBAN) def number : String = r.number From 2583e663c05dd9dccbfb922657a455eb013fd2e5 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 6 Jul 2016 09:07:40 +0200 Subject: [PATCH 593/702] Added updateSingleAccountViews --- .../bankconnectors/KafkaMappedConnector.scala | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index de8369c03..df723a479 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -72,10 +72,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } def setAccountOwner(owner : String, account: KafkaInboundAccount) : Unit = { - logger.error(s"------------> list of all apiusers: ${APIUser.findAll}") - println(s"------------> list of all apiusers: ${APIUser.findAll}") - logger.error(s"------------> owner=${owner}") - println(s"------------> owner=${owner}") val apiUserOwner = APIUser.findAll.find(user => owner == user.emailAddress) logger.info(s"------------> list of all apiusers: ${APIUser.findAll}") logger.info(s"------------> looking for: ${owner}, found: apiUserOwner ${apiUserOwner}") @@ -94,9 +90,23 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } + + def updateSingleAccountViews( apiUser: APIUser, account: KafkaInboundAccount ) = { + val res = { + if (!viewExists(account)) { + val views = createSaveableViews(account) + views.foreach(_.save()) + views.map(_.value).filterNot(_.isPublic).foreach(v => { + Views.views.vend.addPermission(v.uid, apiUser) + logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${apiUser} and account ${account}") + }) + setAccountOwner(apiUser.email.get, account) + } + } + res + } + def updateUserAccountViews( apiUser: APIUser ) = { - logger.info(s"------------> UPDATING VIEWS") - println(s"------------> UPDATING VIEWS") // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId @@ -108,19 +118,12 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable cachedUserAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) } - logger.info(s"------------> userAccounts from Kafka: " + rList) - - for (r <- rList) { logger.info("------------>view " + r + "exists=" + viewExists(r)) } - - - - val res = { // TODO CHECK // for (r <- rList if ! viewExists(r)) yield { @@ -129,7 +132,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable views.foreach(_.save()) views.map(_.value).filterNot(_.isPublic).foreach(v => { Views.views.vend.addPermission(v.uid, apiUser) - logger.info(s"------------> added view: ${v.uid} for apiuser: ${apiUser}") + logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${apiUser}") }) setAccountOwner(apiUser.email.get, r) } @@ -191,8 +194,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable else None - - List(Some(ownerView), publicView, accountantsView, auditorsView).flatten } From 46ff680060b25f44e3aba59c822287d396c91473 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 6 Jul 2016 11:03:02 +0200 Subject: [PATCH 594/702] v2.0.0 createCustomer rules #77 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 13 +++++++++++-- src/main/scala/code/customer/CustomerProvider.scala | 2 ++ .../code/customer/MappedCustomerProvider.scala | 7 +++++++ src/test/scala/code/api/v1_4_0/CustomerTest.scala | 5 +++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 28bf8a387..9ae9f7de2 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1550,13 +1550,20 @@ trait APIMethods200 { for { u <- user ?~! "User must be logged in to post Customer" // TODO. CHECK user has role to create a customer / create a customer for another user id. bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - canCreateCustomer <- Entitlement.entitlement.vend.getEntitlement(bank.bankId.value, u.userId, CanCreateCustomer.toString) ?~ {ErrorMessages.UserDoesNotHaveRole + CanCreateCustomer +"."} postedData <- tryo{json.extract[CreateCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat - isLoggedUser <- booleanToBox(postedData.user_id.nonEmpty == false || postedData.user_id.equalsIgnoreCase(u.userId)) ?~ "User can create a customer for themself only" + canCreateCustomer <- tryo(hasEntitlement(bank.bankId.value, u.userId, CanCreateCustomer)) + isLoggedUser <- booleanToBox(postedData.user_id.nonEmpty == false || canCreateCustomer == true || postedData.user_id.equalsIgnoreCase(u.userId)) ?~ "User can create a customer for themself only" checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists // TODO The user id we expose should be a uuid . For now we have a long direct from the database. user_id <- tryo (if (postedData.user_id.nonEmpty) postedData.user_id else u.userId) ?~ s"Problem getting user_id" customer_user <- User.findByUserId(user_id) ?~! ErrorMessages.UserNotFoundById + userCustomerLinks <- UserCustomerLink.userCustomerLink.vend.getUserCustomerLinks + //Find all user to customer links by user_id + userCustomerLinks <- tryo(userCustomerLinks.filter(u => u.userId.equalsIgnoreCase(user_id))) + customerIds: List[String] <- tryo(userCustomerLinks.map(p => p.customerId)) + //Try to find an existing customer at BANK_ID + alreadyHasCustomer <-booleanToBox(customerIds.forall(x => Customer.customerProvider.vend.getCustomer(x, bank.bankId).isEmpty == true)) ?~ "1.2" //ErrorMessages.CustomerAlreadyExistsForUser + // TODO we still store the user inside the customer, we should only store the user in the usercustomer link customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, customer_user).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser customer <- Customer.customerProvider.vend.addCustomer(bankId, customer_user, @@ -1573,6 +1580,8 @@ trait APIMethods200 { postedData.employment_status, postedData.kyc_status, postedData.last_ok_date) ?~! "Could not create customer" + userCustomerLink <- booleanToBox(UserCustomerLink.userCustomerLink.vend.getUserCustomerLink(user_id, customer.customerId).isEmpty == true) ?~ ErrorMessages.CustomerAlreadyExistsForUser + userCustomerLink <- UserCustomerLink.userCustomerLink.vend.createUserCustomerLink(user_id, customer.customerId, exampleDate, true) ?~! "Could not create user_customer_links" } yield { val successJson = Extraction.decompose(customer) successJsonResponse(successJson, 201) diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala index 054bedb8a..cd3de10b6 100644 --- a/src/main/scala/code/customer/CustomerProvider.scala +++ b/src/main/scala/code/customer/CustomerProvider.scala @@ -19,6 +19,8 @@ trait CustomerProvider { def getCustomerByCustomerId(customerId: String): Box[Customer] + def getCustomer(customerId: String, bankId : BankId): Box[Customer] + def getUser(bankId : BankId, customerNumber : String) : Box[User] def checkCustomerNumberAvailable(bankId : BankId, customerNumber : String) : Boolean diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index 8407d261e..f93cae60f 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -37,6 +37,13 @@ object MappedCustomerProvider extends CustomerProvider { ) } + override def getCustomer(customerId: String, bankId : BankId): Box[Customer] = { + MappedCustomer.find( + By(MappedCustomer.mCustomerId, customerId), + By(MappedCustomer.mBank, bankId.value) + ) + } + override def getUser(bankId: BankId, customerNumber: String): Box[User] = { MappedCustomer.find( By(MappedCustomer.mBank, bankId.value), diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index fdee0d60e..969f5c989 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -43,6 +43,11 @@ class CustomerTest extends V140ServerSetup with DefaultUsers { else Empty } + override def getCustomer(customerId: String, bankId: BankId): Box[Customer] = { + if(customerId == mocCustomerId && bankId == mockBankId) Full(mockCustomer) + else Empty + } + override def getUser(bankId: BankId, customerId: String): Box[User] = Empty override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage, dateOfBirth: Date, From b6f84573fbbfba49d116a86875b7f65850f7aa07 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 6 Jul 2016 11:05:56 +0200 Subject: [PATCH 595/702] v2.0.0 createCustomer rules #77 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 0998a474c..896d962df 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1561,7 +1561,7 @@ trait APIMethods200 { userCustomerLinks <- tryo(userCustomerLinks.filter(u => u.userId.equalsIgnoreCase(user_id))) customerIds: List[String] <- tryo(userCustomerLinks.map(p => p.customerId)) //Try to find an existing customer at BANK_ID - alreadyHasCustomer <-booleanToBox(customerIds.forall(x => Customer.customerProvider.vend.getCustomer(x, bank.bankId).isEmpty == true)) ?~ "1.2" //ErrorMessages.CustomerAlreadyExistsForUser + alreadyHasCustomer <-booleanToBox(customerIds.forall(x => Customer.customerProvider.vend.getCustomer(x, bank.bankId).isEmpty == true)) ?~ ErrorMessages.CustomerAlreadyExistsForUser // TODO we still store the user inside the customer, we should only store the user in the usercustomer link customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, customer_user).isEmpty) ?~ ErrorMessages.CustomerAlreadyExistsForUser customer <- Customer.customerProvider.vend.addCustomer(bankId, From 6eec299bfcf47f095cfefcc4a4581753c4ac02a6 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 6 Jul 2016 13:26:14 +0200 Subject: [PATCH 596/702] Code cleanup --- .../bankconnectors/KafkaMappedConnector.scala | 89 +++++++++++-------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index df723a479..e3754e024 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -91,27 +91,25 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } - def updateSingleAccountViews( apiUser: APIUser, account: KafkaInboundAccount ) = { + def updateSingleAccountViews( user: APIUser, account: KafkaInboundAccount ) = { val res = { - if (!viewExists(account)) { - val views = createSaveableViews(account) - views.foreach(_.save()) - views.map(_.value).filterNot(_.isPublic).foreach(v => { - Views.views.vend.addPermission(v.uid, apiUser) - logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${apiUser} and account ${account}") - }) - setAccountOwner(apiUser.email.get, account) - } + val views = createSaveableViews(account, account.owners.contains(user.email.get)) + views.foreach(_.save()) + views.map(_.value).foreach(v => { + Views.views.vend.addPermission(v.uid, user) + logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${user} and account ${account}") + }) + setAccountOwner(user.email.get, account) } res } - def updateUserAccountViews( apiUser: APIUser ) = { + def updateUserAccountViews( user: APIUser ) = { // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId // in order to fetch corresponding response - val argList = Map("username" -> apiUser.email.get) + val argList = Map("username" -> user.email.get) // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats val rList = { @@ -120,27 +118,20 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable logger.info(s"------------> userAccounts from Kafka: " + rList) - for (r <- rList) { - logger.info("------------>view " + r + "exists=" + viewExists(r)) - } - val res = { - // TODO CHECK - // for (r <- rList if ! viewExists(r)) yield { - for (r <- rList) yield { - val views = createSaveableViews(r) + for (r <- rList) { + val views = createSaveableViews(r, r.owners.contains(user.email.get)) views.foreach(_.save()) views.map(_.value).filterNot(_.isPublic).foreach(v => { - Views.views.vend.addPermission(v.uid, apiUser) - logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${apiUser}") + Views.views.vend.addPermission(v.uid, user) + logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${user}") }) - setAccountOwner(apiUser.email.get, r) + setAccountOwner(user.email.get, r) } } - res } - def updatePublicAccountViews( user: APIUser ): List[List[Saveable[ViewType]]] = { + def updatePublicAccountViews( user: APIUser ) = { // Generate random uuid to be used as request-response match id val reqId: String = UUID.randomUUID().toString // Create argument list with reqId @@ -150,20 +141,25 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val rList = { cachedPublicAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getPublicAccounts", argList).extract[List[KafkaInboundAccount]]) } + + logger.info(s"------------> publicAccounts from Kafka: " + rList) + val res = { - for (r <- rList if ! viewExists(r)) yield { - val views = createSaveableViews(r) + for (r <- rList) { + val views = createSaveableViews(r, ! r.owners.contains(user.email.get)) views.foreach(_.save()) views } } - res } // TODO check me ! (no view name) - def viewExists(acc: KafkaInboundAccount): Boolean = { + def viewExists(acc: KafkaInboundAccount, name: String): Boolean = { + //logger.info(s"------------> all views: " + ViewImpl.findAll) val res = ViewImpl.findAll.filter { v => - if (v.bankPermalink.get == acc.bank && v.accountPermalink.get == acc.id) + if (v.bankPermalink.get == acc.bank && + v.accountPermalink.get == acc.id && + v.name_ == name) true else false @@ -171,30 +167,40 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable res.nonEmpty } - def createSaveableViews(acc : KafkaInboundAccount) : List[Saveable[ViewType]] = { - + def createSaveableViews(acc : KafkaInboundAccount, owner: Boolean = false) : List[Saveable[ViewType]] = { logger.info(s"Kafka createSaveableViews acc is $acc") val bankId = BankId(acc.bank) val accountId = AccountId(acc.id) - val ownerView = createSaveableOwnerView(bankId, accountId) + val ownerView = + if(owner && ! viewExists(acc, "Owner")) Some(createSaveableOwnerView(bankId, accountId)) + else None logger.info(s"Kafka createSaveableViews ownerView is $ownerView") val publicView = - if(acc.generate_public_view) Some(createSaveablePublicView(bankId, accountId)) + if(acc.generate_public_view && ! viewExists(acc, "Public")) { + logger.info("Creating public view") + Some(createSaveablePublicView(bankId, accountId)) + } else None val accountantsView = - if(acc.generate_accountants_view) Some(createSaveableAccountantsView(bankId, accountId)) + if(acc.generate_accountants_view && ! viewExists(acc, "Accountant")) { + logger.info("Creating accountants view") + Some(createSaveableAccountantsView(bankId, accountId)) + } else None val auditorsView = - if(acc.generate_auditors_view) Some(createSaveableAuditorsView(bankId, accountId)) + if(acc.generate_auditors_view && ! viewExists(acc, "Auditor") ) { + logger.info("Creating auditors view") + Some(createSaveableAuditorsView(bankId, accountId)) + } else None - List(Some(ownerView), publicView, accountantsView, auditorsView).flatten + List(ownerView, publicView, accountantsView, auditorsView).flatten } //gets banks handled by this connector @@ -321,7 +327,14 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable cachedAccount.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccount", argList).extract[KafkaInboundAccount]) } val res = new KafkaBankAccount(r) - Full(res) + for { + e <- tryo{OBPUser.getCurrentUserUsername} + u <- OBPUser.getApiUserByEmail(e) + } + yield { + updateSingleAccountViews(u, r) + } + Full(res) } override def getBankAccounts(accts: List[(BankId, AccountId)]): List[KafkaBankAccount] = { From 58b64e4cb448aa8234488cd026d982805956a22d Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 6 Jul 2016 13:37:22 +0200 Subject: [PATCH 597/702] comment on kafka account label / number --- src/main/scala/code/bankconnectors/KafkaMappedConnector.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index de8369c03..7f678e081 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -995,7 +995,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable def balance : BigDecimal = BigDecimal(r.balance.amount) def currency : String = r.balance.currency def name : String = r.owners.head - def label : String = r.number + def label : String = r.number // Temp (label should be writable so customer can change) def swift_bic : Option[String] = Some("swift_bic") //TODO def iban : Option[String] = Some(r.IBAN) def number : String = r.number From 2cedb5832e2f37db6aaeeea4b960021fe2a70253 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 7 Jul 2016 12:38:37 +0200 Subject: [PATCH 598/702] End point /users #80 --- .../scala/code/api/v2_0_0/APIMethods200.scala | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 896d962df..c2ad20463 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1341,13 +1341,19 @@ trait APIMethods200 { .email(postedData.email) .password(postedData.password) .validated(true) // TODO Get this from Props - .saveMe() - if (userCreated.saved_?) { - val json = JSONFactory200.createUserJSONfromOBPUser(userCreated) - successJsonResponse(Extraction.decompose(json), 201) + if(userCreated.validate.size > 0){ + Full(errorJsonResponse(userCreated.validate.map(_.msg).mkString(";"))) } else - Full(errorJsonResponse("Error occurred during user creation.")) + { + userCreated.saveMe() + if (userCreated.saved_?) { + val json = JSONFactory200.createUserJSONfromOBPUser(userCreated) + successJsonResponse(Extraction.decompose(json), 201) + } + else + Full(errorJsonResponse("Error occurred during user creation.")) + } } else { Full(errorJsonResponse("User with the same email already exists.")) From 07f38771eaa44103e1aa8e2e0619eb9fc66226c0 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 7 Jul 2016 19:59:25 +0200 Subject: [PATCH 599/702] Code cleanup --- .../bankconnectors/KafkaMappedConnector.scala | 195 +++++++++--------- 1 file changed, 97 insertions(+), 98 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index e3754e024..7aa78af7b 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -71,17 +71,28 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } + def accountOwnerExists(user: APIUser, account: KafkaInboundAccount): Boolean = { + val res = + MappedAccountHolder.findAll( + By(MappedAccountHolder.user, user), + By(MappedAccountHolder.accountBankPermalink, account.bank), + By(MappedAccountHolder.accountPermalink, account.id) + ) + + res.nonEmpty + } + def setAccountOwner(owner : String, account: KafkaInboundAccount) : Unit = { val apiUserOwner = APIUser.findAll.find(user => owner == user.emailAddress) - logger.info(s"------------> list of all apiusers: ${APIUser.findAll}") - logger.info(s"------------> looking for: ${owner}, found: apiUserOwner ${apiUserOwner}") apiUserOwner match { case Some(o) => { - val holder = MappedAccountHolder.create - .user(o) - .accountBankPermalink(account.bank) - .accountPermalink(account.id).saveMe - logger.info(s"------------> created mappeduserholder: ${holder}") + if ( ! accountOwnerExists(o, account)) { + val holder = MappedAccountHolder.create + .user(o) + .accountBankPermalink(account.bank) + .accountPermalink(account.id).saveMe + logger.info(s"------------> created mappedUserHolder ${holder}") + } } case None => { //This shouldn't happen as OBPUser should generate the APIUsers when saved @@ -90,44 +101,43 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } - def updateSingleAccountViews( user: APIUser, account: KafkaInboundAccount ) = { - val res = { - val views = createSaveableViews(account, account.owners.contains(user.email.get)) + for { + email <- tryo{user.emailAddress} + views <- tryo{createSaveableViews(account, account.owners.contains(email))} + } yield { views.foreach(_.save()) views.map(_.value).foreach(v => { Views.views.vend.addPermission(v.uid, user) - logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${user} and account ${account}") + logger.info(s"------------> added view ${v.name} for apiuser ${user} and account ${account}") }) - setAccountOwner(user.email.get, account) + setAccountOwner(email, account) } - res } + implicit val formats = net.liftweb.json.DefaultFormats + def updateUserAccountViews( user: APIUser ) = { - // Generate random uuid to be used as request-response match id - val reqId: String = UUID.randomUUID().toString - // Create argument list with reqId - // in order to fetch corresponding response - val argList = Map("username" -> user.email.get) - // Since result is single account, we need only first list entry - implicit val formats = net.liftweb.json.DefaultFormats - val rList = { - cachedUserAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) - } + val accounts = for { + email <- tryo {user.emailAddress} + argList <- tryo {Map[String, String]("username" -> email)} + // Generate random uuid to be used as request-response match id + reqId <- tryo {UUID.randomUUID().toString} + } yield { + cachedUserAccounts.getOrElseUpdate(argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) + }.head - logger.info(s"------------> userAccounts from Kafka: " + rList) + val views = for { + acc <- accounts + email <- tryo {user.emailAddress} + views <- tryo {createSaveableViews(acc, acc.owners.contains(email))} + } yield { + setAccountOwner(email, acc) + views.foreach(_.save()) + views.map(_.value).filterNot(_.isPublic).foreach(v => { + logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${user}") - val res = { - for (r <- rList) { - val views = createSaveableViews(r, r.owners.contains(user.email.get)) - views.foreach(_.save()) - views.map(_.value).filterNot(_.isPublic).foreach(v => { - Views.views.vend.addPermission(v.uid, user) - logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${user}") - }) - setAccountOwner(user.email.get, r) - } + }) } } @@ -153,17 +163,13 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } - // TODO check me ! (no view name) - def viewExists(acc: KafkaInboundAccount, name: String): Boolean = { - //logger.info(s"------------> all views: " + ViewImpl.findAll) - val res = ViewImpl.findAll.filter { v => - if (v.bankPermalink.get == acc.bank && - v.accountPermalink.get == acc.id && - v.name_ == name) - true - else - false - } + def viewExists(account: KafkaInboundAccount, name: String): Boolean = { + val res = + ViewImpl.findAll( + By(ViewImpl.bankPermalink, account.bank), + By(ViewImpl.accountPermalink, account.id), + By(ViewImpl.name_, name) + ) res.nonEmpty } @@ -173,11 +179,12 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val accountId = AccountId(acc.id) val ownerView = - if(owner && ! viewExists(acc, "Owner")) Some(createSaveableOwnerView(bankId, accountId)) + if(owner && ! viewExists(acc, "Owner")) { + logger.info("Creating owner view") + Some(createSaveableOwnerView(bankId, accountId)) + } else None - logger.info(s"Kafka createSaveableViews ownerView is $ownerView") - val publicView = if(acc.generate_public_view && ! viewExists(acc, "Public")) { logger.info("Creating public view") @@ -268,12 +275,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Since result is single account, we need only first list entry implicit val formats = net.liftweb.json.DefaultFormats val r = process(reqId, "getTransaction", argList).extract[KafkaInboundTransaction] - // If empty result from Kafka return empty data - if (r.id == "") { - return Failure(null) - } - //updateAccountTransactions(bankId, accountID) - Full(createNewTransaction(r)) + createNewTransaction(r) } override def getTransactions(bankId: BankId, accountID: AccountId, queryParams: OBPQueryParam*): Box[List[Transaction]] = { @@ -299,18 +301,14 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable "queryParams" -> queryParams.toString ) implicit val formats = net.liftweb.json.DefaultFormats val rList = process(reqId, "getTransactions", argList).extract[List[KafkaInboundTransaction]] - // Return blank if empty - if (rList.head.id == "") { - return Failure(null) - } // Populate fields and generate result - val res = { - for (r <- rList) yield { - createNewTransaction(r) - } - } + Full( for { + r <- rList + transaction <- createNewTransaction(r) + } yield { + transaction + }) //updateAccountTransactions(bankId, accountID) - return Full(res) } override def getBankAccount(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { @@ -921,12 +919,12 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable def process(reqId: String, command: String, argList: Map[String,String]): json.JValue = { //List[Map[String,String]] = { var retries:Int = 3 while (consumer == null && retries > 0 ) { - retries-=1 + retries -= 1 consumer = new KafkaConsumer() } retries = 3 while (producer == null && retries > 0) { - retries-=1 + retries -= 1 producer = new KafkaProducer() } if (producer == null || consumer == null) @@ -940,7 +938,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Helper for creating a transaction - def createNewTransaction(r: KafkaInboundTransaction) = { + def createNewTransaction(r: KafkaInboundTransaction):Box[Transaction] = { var datePosted: Date = null if (r.details.posted != null) // && r.details.posted.matches("^[0-9]{8}$")) datePosted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.posted) @@ -949,29 +947,30 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable if (r.details.completed != null) // && r.details.completed.matches("^[0-9]{8}$")) dateCompleted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.completed) - val c = getBankAccount(BankId(r.this_account.bank), AccountId(r.counterparty.get.account_number.get)).orNull - val o = getBankAccount(BankId(r.this_account.bank), AccountId(r.this_account.id)).orNull - //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) - val dummyOtherBankAccount = createOtherBankAccount(c, o, None) - //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object - //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init - val otherAccount = createOtherBankAccount(c, o, Some(dummyOtherBankAccount.metadata)) - - // Create new transaction - new Transaction( - r.id, // uuid:String - TransactionId(r.id), // id:TransactionId - getBankAccount( BankId(r.this_account.bank), - AccountId(r.this_account.id)).orNull, // thisAccount:BankAccount - otherAccount, // otherAccount:OtherBankAccount - r.details.`type`, // transactionType:String - BigDecimal(r.details.value), // val amount:BigDecimal - o.currency, // currency:String - Some(r.details.description), // description:Option[String] - datePosted, // startDate:Date - dateCompleted, // finishDate:Date - BigDecimal(r.details.new_balance) // balance:BigDecimal - ) + for { + counterparty <- r.counterparty + thisAccount <- getBankAccount(BankId(r.this_account.bank), AccountId(r.this_account.id)) + //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) + dummyOtherBankAccount <- tryo{createOtherBankAccount(counterparty, thisAccount, None)} + //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object + //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init + otherAccount <- tryo{createOtherBankAccount(counterparty, thisAccount, Some(dummyOtherBankAccount.metadata))} + } yield { + // Create new transaction + new Transaction( + r.id, // uuid:String + TransactionId(r.id), // id:TransactionId + thisAccount, // thisAccount:BankAccount + otherAccount, // otherAccount:OtherBankAccount + r.details.`type`, // transactionType:String + BigDecimal(r.details.value), // val amount:BigDecimal + thisAccount.currency, // currency:String + Some(r.details.description), // description:Option[String] + datePosted, // startDate:Date + dateCompleted, // finishDate:Date + BigDecimal(r.details.new_balance) // balance:BigDecimal) + ) + } } @@ -986,16 +985,16 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } // Helper for creating other bank account - def createOtherBankAccount(c: KafkaBankAccount, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { + def createOtherBankAccount(c: KafkaInboundTransactionCounterparty, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { new OtherBankAccount( id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), - label = c.label, - nationalIdentifier = c.nationalIdentifier, //TODO - swift_bic = Some(c.swift_bic.get), //TODO - iban = Some(c.iban.get), - number = c.number, - bankName = c.bankName, - kind = c.accountType, + label = c.name + " " + c.account_number, + nationalIdentifier = "", + swift_bic = None, + iban = None, + number = "" + c.account_number, + bankName = "", + kind = "", originalPartyBankId = BankId(o.bankId.value), originalPartyAccountId = AccountId(o.accountId.value), alreadyFoundMetadata = alreadyFoundMetadata From 493b374f5983c58ffded5b41f4848a6f08e3855c Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Thu, 7 Jul 2016 20:35:10 +0200 Subject: [PATCH 600/702] Fixed getTransactions --- .../bankconnectors/KafkaMappedConnector.scala | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 11a5b514c..77391d131 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -302,13 +302,14 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable implicit val formats = net.liftweb.json.DefaultFormats val rList = process(reqId, "getTransactions", argList).extract[List[KafkaInboundTransaction]] // Populate fields and generate result - Full( for { + val res = for { r <- rList transaction <- createNewTransaction(r) } yield { transaction - }) - //updateAccountTransactions(bankId, accountID) + } + Full(res) + //TODO is this needed updateAccountTransactions(bankId, accountID) } override def getBankAccount(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = { @@ -948,13 +949,13 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable dateCompleted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(r.details.completed) for { - counterparty <- r.counterparty + counterparty <- tryo{r.counterparty} thisAccount <- getBankAccount(BankId(r.this_account.bank), AccountId(r.this_account.id)) //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) - dummyOtherBankAccount <- tryo{createOtherBankAccount(counterparty, thisAccount, None)} + dummyOtherBankAccount <- tryo{createOtherBankAccount(counterparty.get, thisAccount, None)} //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init - otherAccount <- tryo{createOtherBankAccount(counterparty, thisAccount, Some(dummyOtherBankAccount.metadata))} + otherAccount <- tryo{createOtherBankAccount(counterparty.get, thisAccount, Some(dummyOtherBankAccount.metadata))} } yield { // Create new transaction new Transaction( @@ -988,11 +989,11 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable def createOtherBankAccount(c: KafkaInboundTransactionCounterparty, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { new OtherBankAccount( id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), - label = c.name + " " + c.account_number, + label = c.name.getOrElse("") + " " + c.account_number.getOrElse(""), nationalIdentifier = "", swift_bic = None, iban = None, - number = "" + c.account_number, + number = c.account_number.getOrElse("").substring(0,30), bankName = "", kind = "", originalPartyBankId = BankId(o.bankId.value), From e26cc57efbbb54e1780939128a45e5e16354aedb Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 8 Jul 2016 05:44:17 +0200 Subject: [PATCH 601/702] Adding some comments on Views and ViewPrivileges --- src/main/scala/code/model/View.scala | 6 +++++- src/main/scala/code/model/dataAccess/view.scala | 4 ++++ src/main/scala/code/views/MapperViews.scala | 8 +++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/model/View.scala b/src/main/scala/code/model/View.scala index fab466bd4..3deca6486 100644 --- a/src/main/scala/code/model/View.scala +++ b/src/main/scala/code/model/View.scala @@ -52,6 +52,10 @@ case class Permission( views : List[View] ) + +/* +View Specification + */ trait ViewData { def description: String def is_public: Boolean @@ -139,8 +143,8 @@ case class ViewUpdateData( * @define canSeePhysicalLocation If true, the view will show the Counterparty PhysicalLocation * @define canSeePublicAlias If true, the view will show the Counterparty PublicAlias * @define canSeePrivateAlias If true, the view will show the Counterparty PrivateAlias - * @define canAddMoreInfo If true, the view will show the Counterparty AddMoreInfo * + * @define canAddMoreInfo If true, the view can add the Counterparty MoreInfo * @define canAddURL If true, the view can add the Counterparty Url * @define canAddImageURL If true, the view can add the Counterparty Image Url * @define canAddOpenCorporatesUrl If true, the view can add the Counterparty OpenCorporatesUrl diff --git a/src/main/scala/code/model/dataAccess/view.scala b/src/main/scala/code/model/dataAccess/view.scala index 3a0074391..02b5ad2a6 100644 --- a/src/main/scala/code/model/dataAccess/view.scala +++ b/src/main/scala/code/model/dataAccess/view.scala @@ -38,6 +38,10 @@ import net.liftweb.mapper._ import code.model._ import scala.collection.immutable.List +/* +This stores the link between A User and a View +A User can't use a View unless it is listed here. + */ class ViewPrivileges extends LongKeyedMapper[ViewPrivileges] with IdPK with CreatedUpdated { def getSingleton = ViewPrivileges object user extends MappedLongForeignKey(this, APIUser) diff --git a/src/main/scala/code/views/MapperViews.scala b/src/main/scala/code/views/MapperViews.scala index 30cfcfbcb..a9be13cb0 100644 --- a/src/main/scala/code/views/MapperViews.scala +++ b/src/main/scala/code/views/MapperViews.scala @@ -46,6 +46,7 @@ private object MapperViews extends Views with Loggable { } def addPermission(viewUID: ViewUID, user: User): Box[View] = { + logger.debug(s"addPermission says viewUID is $viewUID user is $user") val viewImpl = ViewImpl.find(viewUID) viewImpl match { @@ -166,9 +167,12 @@ private object MapperViews extends Views with Loggable { ViewImpl.find(viewUID) } + /* + Create View based on the Specification (name, alias behavior, what fields can be seen, actions are allowed etc. ) + * */ def createView(bankAccount: BankAccount, view: ViewCreationJSON): Box[View] = { if(view.name.contentEquals("")) { - return Failure("It is not allowed to create a view with an empty name") + return Failure("You cannot create a View with an empty Name") } val newViewPermalink = { @@ -194,6 +198,8 @@ private object MapperViews extends Views with Loggable { } } + + /* Update the specification of the view (what data/actions are allowed) */ def updateView(bankAccount : BankAccount, viewId: ViewId, viewUpdateJson : ViewUpdateData) : Box[View] = { for { From 12ab884996af5e54b075e00df25f4f0632eb103e Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 8 Jul 2016 06:05:47 +0200 Subject: [PATCH 602/702] Refactor: Rename ViewData -> ViewSpecification and related Rename ViewData -> ViewSpecification, ViewCreationJSON -> CreateViewJSON, ViewUpdateData -> UpdateViewJSON --- src/main/scala/code/api/v1_2/OBPAPI1.2.scala | 4 ++-- src/main/scala/code/api/v1_2_1/APIMethods121.scala | 12 ++++++------ src/main/scala/code/model/BankingData.scala | 4 ++-- src/main/scala/code/model/View.scala | 10 +++++----- src/main/scala/code/model/dataAccess/view.scala | 2 +- src/main/scala/code/views/MapperViews.scala | 6 +++--- src/main/scala/code/views/Views.scala | 6 +++--- src/test/scala/code/api/API121Test.scala | 14 +++++++------- src/test/scala/code/api/API12Test.scala | 12 ++++++------ 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/main/scala/code/api/v1_2/OBPAPI1.2.scala b/src/main/scala/code/api/v1_2/OBPAPI1.2.scala index 06108927d..54aac70df 100644 --- a/src/main/scala/code/api/v1_2/OBPAPI1.2.scala +++ b/src/main/scala/code/api/v1_2/OBPAPI1.2.scala @@ -203,7 +203,7 @@ object OBPAPI1_2 extends OBPRestHelper with Loggable { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: Nil JsonPost json -> _ => { user => for { - json <- tryo{json.extract[ViewCreationJSON]} ?~ "wrong JSON format" + json <- tryo{json.extract[CreateViewJSON]} ?~ "wrong JSON format" u <- user ?~ "user not found" account <- BankAccount(bankId, accountId) view <- account createView (u, json) @@ -221,7 +221,7 @@ object OBPAPI1_2 extends OBPRestHelper with Loggable { for { account <- BankAccount(bankId, accountId) u <- user ?~ "user not found" - updateJson <- tryo{json.extract[ViewUpdateData]} ?~ "wrong JSON format" + updateJson <- tryo{json.extract[UpdateViewJSON]} ?~ "wrong JSON format" updatedView <- account.updateView(u, viewId, updateJson) } yield { val viewJSON = JSONFactory.createViewJSON(updatedView) diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index bc2406a0c..c418ff9a7 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -16,9 +16,9 @@ import code.bankconnectors._ import code.bankconnectors.OBPOffset import code.bankconnectors.OBPFromDate import code.bankconnectors.OBPToDate -import code.model.ViewCreationJSON +import code.model.CreateViewJSON import net.liftweb.common.Full -import code.model.ViewUpdateData +import code.model.UpdateViewJSON import scala.collection.immutable.Nil import scala.collection.mutable.ArrayBuffer @@ -532,7 +532,7 @@ trait APIMethods121 { | 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 'allowed_actions' field is a list containing the name of the actions allowed on this view, all the actions contained will be set to `true` on the view creation, the rest will be set to `false`.""", - Extraction.decompose(ViewCreationJSON("Name of view to create", "Description of view (this example is public, uses the public alias, and has limited access to account data)", true, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label", "can_see_tags"))), + Extraction.decompose(CreateViewJSON("Name of view to create", "Description of view (this example is public, uses the public alias, and has limited access to account data)", true, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label", "can_see_tags"))), emptyObjectJson, emptyObjectJson :: Nil, false, @@ -546,7 +546,7 @@ trait APIMethods121 { user => for { u <- user ?~ "user not found" - json <- tryo{json.extract[ViewCreationJSON]} ?~ "wrong JSON format" + json <- tryo{json.extract[CreateViewJSON]} ?~ "wrong JSON format" account <- BankAccount(bankId, accountId) view <- account createView (u, json) } yield { @@ -569,7 +569,7 @@ trait APIMethods121 { | |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)""", - Extraction.decompose(ViewUpdateData("New description of view", false, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label"))), + Extraction.decompose(UpdateViewJSON("New description of view", false, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label"))), emptyObjectJson, emptyObjectJson :: Nil, false, @@ -584,7 +584,7 @@ trait APIMethods121 { for { account <- BankAccount(bankId, accountId) u <- user ?~ "user not found" - updateJson <- tryo{json.extract[ViewUpdateData]} ?~ "wrong JSON format" + updateJson <- tryo{json.extract[UpdateViewJSON]} ?~ "wrong JSON format" updatedView <- account.updateView(u, viewId, updateJson) } yield { val viewJSON = JSONFactory.createViewJSON(updatedView) diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 825d7564f..d27b23541 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -436,7 +436,7 @@ trait BankAccount { Failure("user : " + user.emailAddress + " don't have access to owner view on account " + accountId, Empty, Empty) } - final def createView(userDoingTheCreate : User,v: ViewCreationJSON): Box[View] = { + final def createView(userDoingTheCreate : User,v: CreateViewJSON): Box[View] = { if(!userDoingTheCreate.ownerAccess(this)) { Failure({"user: " + userDoingTheCreate.idGivenByProvider + " at provider " + userDoingTheCreate.provider + " does not have owner access"}) } else { @@ -451,7 +451,7 @@ trait BankAccount { } } - final def updateView(userDoingTheUpdate : User, viewId : ViewId, v: ViewUpdateData) : Box[View] = { + final def updateView(userDoingTheUpdate : User, viewId : ViewId, v: UpdateViewJSON) : Box[View] = { if(!userDoingTheUpdate.ownerAccess(this)) { Failure({"user: " + userDoingTheUpdate.idGivenByProvider + " at provider " + userDoingTheUpdate.provider + " does not have owner access"}) } else { diff --git a/src/main/scala/code/model/View.scala b/src/main/scala/code/model/View.scala index 3deca6486..4d55301af 100644 --- a/src/main/scala/code/model/View.scala +++ b/src/main/scala/code/model/View.scala @@ -56,7 +56,7 @@ case class Permission( /* View Specification */ -trait ViewData { +trait ViewSpecification { def description: String def is_public: Boolean def which_alias_to_use: String @@ -64,21 +64,21 @@ trait ViewData { def allowed_actions : List[String] } -case class ViewCreationJSON( +case class CreateViewJSON( name: String, description: String, is_public: Boolean, which_alias_to_use: String, hide_metadata_if_alias_used: Boolean, allowed_actions : List[String] -) extends ViewData +) extends ViewSpecification -case class ViewUpdateData( +case class UpdateViewJSON( description: String, is_public: Boolean, which_alias_to_use: String, hide_metadata_if_alias_used: Boolean, - allowed_actions: List[String]) extends ViewData + allowed_actions: List[String]) extends ViewSpecification diff --git a/src/main/scala/code/model/dataAccess/view.scala b/src/main/scala/code/model/dataAccess/view.scala index 02b5ad2a6..d2b0e42f3 100644 --- a/src/main/scala/code/model/dataAccess/view.scala +++ b/src/main/scala/code/model/dataAccess/view.scala @@ -67,7 +67,7 @@ class ViewImpl extends View with LongKeyedMapper[ViewImpl] with ManyToMany with def users : List[User] = users_.toList //Important! If you add a field, be sure to handle it here in this function - def setFromViewData(viewData : ViewData) = { + def setFromViewData(viewData : ViewSpecification) = { if(viewData.which_alias_to_use == "public"){ usePublicAliasIfOneExists_(true) diff --git a/src/main/scala/code/views/MapperViews.scala b/src/main/scala/code/views/MapperViews.scala index a9be13cb0..2ee83df93 100644 --- a/src/main/scala/code/views/MapperViews.scala +++ b/src/main/scala/code/views/MapperViews.scala @@ -3,7 +3,7 @@ package code.views import code.api.APIFailure import code.bankconnectors.Connector import code.model.dataAccess.{ViewImpl, ViewPrivileges} -import code.model.{Permission, ViewCreationJSON, ViewUpdateData, _} +import code.model.{Permission, CreateViewJSON, UpdateViewJSON, _} import net.liftweb.common._ import net.liftweb.mapper.By @@ -170,7 +170,7 @@ private object MapperViews extends Views with Loggable { /* Create View based on the Specification (name, alias behavior, what fields can be seen, actions are allowed etc. ) * */ - def createView(bankAccount: BankAccount, view: ViewCreationJSON): Box[View] = { + def createView(bankAccount: BankAccount, view: CreateViewJSON): Box[View] = { if(view.name.contentEquals("")) { return Failure("You cannot create a View with an empty Name") } @@ -200,7 +200,7 @@ private object MapperViews extends Views with Loggable { /* Update the specification of the view (what data/actions are allowed) */ - def updateView(bankAccount : BankAccount, viewId: ViewId, viewUpdateJson : ViewUpdateData) : Box[View] = { + def updateView(bankAccount : BankAccount, viewId: ViewId, viewUpdateJson : UpdateViewJSON) : Box[View] = { for { view <- ViewImpl.find(viewId, bankAccount) diff --git a/src/main/scala/code/views/Views.scala b/src/main/scala/code/views/Views.scala index bad8b6c9d..c89013e16 100644 --- a/src/main/scala/code/views/Views.scala +++ b/src/main/scala/code/views/Views.scala @@ -4,7 +4,7 @@ import net.liftweb.common.Box import code.model._ import net.liftweb.util.SimpleInjector import code.model.Permission -import code.model.ViewCreationJSON +import code.model.CreateViewJSON object Views extends SimpleInjector { @@ -26,9 +26,9 @@ trait Views { def view(viewId : ViewId, bankAccount: BankAccount) : Box[View] def view(viewUID : ViewUID) : Box[View] - def createView(bankAccount : BankAccount, view: ViewCreationJSON) : Box[View] + def createView(bankAccount : BankAccount, view: CreateViewJSON) : Box[View] def removeView(viewId : ViewId, bankAccount: BankAccount): Box[Unit] - def updateView(bankAccount : BankAccount, viewId : ViewId, viewUpdateJson : ViewUpdateData) : Box[View] + def updateView(bankAccount : BankAccount, viewId : ViewId, viewUpdateJson : UpdateViewJSON) : Box[View] def views(bankAccount : BankAccount) : List[View] def permittedViews(user: User, bankAccount: BankAccount): List[View] def publicViews(bankAccount : BankAccount) : List[View] diff --git a/src/test/scala/code/api/API121Test.scala b/src/test/scala/code/api/API121Test.scala index f4a886a3b..3ffaadb09 100644 --- a/src/test/scala/code/api/API121Test.scala +++ b/src/test/scala/code/api/API121Test.scala @@ -243,8 +243,8 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser viewsIdsToGrant } - def randomView(isPublic: Boolean, alias: String) : ViewCreationJSON = { - ViewCreationJSON( + def randomView(isPublic: Boolean, alias: String) : CreateViewJSON = { + CreateViewJSON( name = randomString(3), description = randomString(3), is_public = isPublic, @@ -315,12 +315,12 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser makeGetRequest(request) } - def postView(bankId: String, accountId: String, view: ViewCreationJSON, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { + def postView(bankId: String, accountId: String, view: CreateViewJSON, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { val request = (v1_2Request / "banks" / bankId / "accounts" / accountId / "views").POST <@(consumerAndToken) makePostRequest(request, write(view)) } - def putView(bankId: String, accountId: String, viewId : String, view: ViewUpdateData, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { + def putView(bankId: String, accountId: String, viewId : String, view: UpdateViewJSON, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { val request = (v1_2Request / "banks" / bankId / "accounts" / accountId / "views" / viewId).PUT <@(consumerAndToken) makePutRequest(request, write(view)) } @@ -1465,7 +1465,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser val bankId = randomBank val bankAccount : AccountJSON = randomPrivateAccount(bankId) val viewsBefore = getAccountViews(bankId, bankAccount.id, user1).body.extract[ViewsJSON].views - val viewWithEmptyName = ViewCreationJSON( + val viewWithEmptyName = CreateViewJSON( name = "", description = randomString(3), is_public = true, @@ -1491,7 +1491,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser def viewUpdateJson(originalView : ViewJSON) = { //it's not perfect, assumes too much about originalView (i.e. randomView(true, "")) - new ViewUpdateData( + new UpdateViewJSON( description = updatedViewDescription, is_public = !originalView.is_public, which_alias_to_use = updatedAliasToUse, @@ -1501,7 +1501,7 @@ class API1_2_1Test extends User1AllPrivileges with DefaultUsers with PrivateUser } def someViewUpdateJson() = { - new ViewUpdateData( + new UpdateViewJSON( description = updatedViewDescription, is_public = true, which_alias_to_use = updatedAliasToUse, diff --git a/src/test/scala/code/api/API12Test.scala b/src/test/scala/code/api/API12Test.scala index 650c0f57a..30e8eceb9 100644 --- a/src/test/scala/code/api/API12Test.scala +++ b/src/test/scala/code/api/API12Test.scala @@ -231,8 +231,8 @@ class API1_2Test extends User1AllPrivileges with DefaultUsers { viewsIdsToGrant } - def randomView(isPublic: Boolean, alias: String) : ViewCreationJSON = { - ViewCreationJSON( + def randomView(isPublic: Boolean, alias: String) : CreateViewJSON = { + CreateViewJSON( name = randomString(3), description = randomString(3), is_public = isPublic, @@ -286,12 +286,12 @@ class API1_2Test extends User1AllPrivileges with DefaultUsers { makeGetRequest(request) } - def postView(bankId: String, accountId: String, view: ViewCreationJSON, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { + def postView(bankId: String, accountId: String, view: CreateViewJSON, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { val request = (v1_2Request / "banks" / bankId / "accounts" / accountId / "views").POST <@(consumerAndToken) makePostRequest(request, write(view)) } - def putView(bankId: String, accountId: String, viewId : String, view: ViewUpdateData, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { + def putView(bankId: String, accountId: String, viewId : String, view: UpdateViewJSON, consumerAndToken: Option[(Consumer, Token)]): APIResponse = { val request = (v1_2Request / "banks" / bankId / "accounts" / accountId / "views" / viewId).PUT <@(consumerAndToken) makePutRequest(request, write(view)) } @@ -964,7 +964,7 @@ class API1_2Test extends User1AllPrivileges with DefaultUsers { def viewUpdateJson(originalView : ViewJSON) = { //it's not perfect, assumes too much about originalView (i.e. randomView(true, "")) - new ViewUpdateData( + new UpdateViewJSON( description = updatedViewDescription, is_public = !originalView.is_public, which_alias_to_use = updatedAliasToUse, @@ -974,7 +974,7 @@ class API1_2Test extends User1AllPrivileges with DefaultUsers { } def someViewUpdateJson() = { - new ViewUpdateData( + new UpdateViewJSON( description = updatedViewDescription, is_public = true, which_alias_to_use = updatedAliasToUse, From ccbf5c6ff24700f0e201d8beb0ba3a568aa2400b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 8 Jul 2016 06:15:26 +0200 Subject: [PATCH 603/702] Comments on View creation JSON etc. --- src/main/scala/code/model/View.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/model/View.scala b/src/main/scala/code/model/View.scala index 4d55301af..79028cec2 100644 --- a/src/main/scala/code/model/View.scala +++ b/src/main/scala/code/model/View.scala @@ -55,6 +55,7 @@ case class Permission( /* View Specification +Defines how the View should be named, i.e. if it is public, the Alias behaviour, what fields can be seen and what actions can be done through it. */ trait ViewSpecification { def description: String @@ -64,6 +65,9 @@ trait ViewSpecification { def allowed_actions : List[String] } +/* +The JSON used during creation of the view. See ViewSpecification + */ case class CreateViewJSON( name: String, description: String, @@ -73,6 +77,9 @@ case class CreateViewJSON( allowed_actions : List[String] ) extends ViewSpecification +/* +The JSON used to update the specification of the view. See ViewSpecification + */ case class UpdateViewJSON( description: String, is_public: Boolean, @@ -246,7 +253,7 @@ trait View { def canSeePublicAlias : Boolean def canSeePrivateAlias : Boolean - //other bank account meta data - write + //other bank account (Counterparty) meta data - write def canAddMoreInfo : Boolean def canAddURL : Boolean def canAddImageURL : Boolean From 5d839e99eac9cb1790ddb5f3122f7934727a2d24 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 8 Jul 2016 13:19:25 +0200 Subject: [PATCH 604/702] Fixed views creation in kafka connector --- .../bankconnectors/KafkaMappedConnector.scala | 40 +++++-------------- .../scala/code/model/dataAccess/OBPUser.scala | 1 - 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 77391d131..faeeecc3e 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -101,7 +101,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } - def updateSingleAccountViews( user: APIUser, account: KafkaInboundAccount ) = { + def updateAccountViews(user: APIUser, account: KafkaInboundAccount ) = { for { email <- tryo{user.emailAddress} views <- tryo{createSaveableViews(account, account.owners.contains(email))} @@ -109,7 +109,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable views.foreach(_.save()) views.map(_.value).foreach(v => { Views.views.vend.addPermission(v.uid, user) - logger.info(s"------------> added view ${v.name} for apiuser ${user} and account ${account}") + logger.info(s"------------> updated view ${v.uid} for apiuser ${user} and account ${account}") }) setAccountOwner(email, account) } @@ -125,44 +125,23 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable reqId <- tryo {UUID.randomUUID().toString} } yield { cachedUserAccounts.getOrElseUpdate(argList.toString, () => process(reqId, "getUserAccounts", argList).extract[List[KafkaInboundAccount]]) - }.head + } val views = for { - acc <- accounts + acc <- accounts.getOrElse(List.empty) email <- tryo {user.emailAddress} views <- tryo {createSaveableViews(acc, acc.owners.contains(email))} } yield { setAccountOwner(email, acc) views.foreach(_.save()) - views.map(_.value).filterNot(_.isPublic).foreach(v => { - logger.info(s"------------> added view: ${v.name}, ${v.uid} for apiuser: ${user}") - + //views.map(_.value).filterNot(_.isPublic).foreach(v => { + views.map(_.value).foreach(v => { + Views.views.vend.addPermission(v.uid, user) + logger.info(s"------------> added view ${v.uid} for apiuser ${user} and account ${acc}") }) } } - def updatePublicAccountViews( user: APIUser ) = { - // Generate random uuid to be used as request-response match id - val reqId: String = UUID.randomUUID().toString - // Create argument list with reqId - // in order to fetch corresponding response - val argList = Map("username" -> user.email.get ) - implicit val formats = net.liftweb.json.DefaultFormats - val rList = { - cachedPublicAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getPublicAccounts", argList).extract[List[KafkaInboundAccount]]) - } - - logger.info(s"------------> publicAccounts from Kafka: " + rList) - - val res = { - for (r <- rList) { - val views = createSaveableViews(r, ! r.owners.contains(user.email.get)) - views.foreach(_.save()) - views - } - } - } - def viewExists(account: KafkaInboundAccount, name: String): Boolean = { val res = ViewImpl.findAll( @@ -206,7 +185,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } else None - List(ownerView, publicView, accountantsView, auditorsView).flatten } @@ -331,7 +309,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable u <- OBPUser.getApiUserByEmail(e) } yield { - updateSingleAccountViews(u, r) + updateAccountViews(u, r) } Full(res) } diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 317104a42..5805cfc0f 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -389,7 +389,6 @@ import net.liftweb.util.Helpers._ if (!extUser.isEmpty) { val u = APIUser.find(By(APIUser.email, extUser.getOrElse(null).email)).getOrElse(null) if (u != null) { - //KafkaMappedConnector.updatePublicAccountViews(u) KafkaMappedConnector.updateUserAccountViews(u) } From 49de2ee834d62135bc81366831ac17e6a9958eeb Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 8 Jul 2016 13:20:17 +0200 Subject: [PATCH 605/702] Comment on CreateViewJSON --- src/main/scala/code/model/View.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/model/View.scala b/src/main/scala/code/model/View.scala index 79028cec2..5bd384595 100644 --- a/src/main/scala/code/model/View.scala +++ b/src/main/scala/code/model/View.scala @@ -66,7 +66,7 @@ trait ViewSpecification { } /* -The JSON used during creation of the view. See ViewSpecification +The JSON that should be supplied to create a view. Conforms to ViewSpecification */ case class CreateViewJSON( name: String, @@ -78,7 +78,7 @@ case class CreateViewJSON( ) extends ViewSpecification /* -The JSON used to update the specification of the view. See ViewSpecification +The JSON that should be supplied to update a view. Conforms to ViewSpecification */ case class UpdateViewJSON( description: String, From 3d990ce2550282b198ddc24170cf7f6e9041fb58 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 8 Jul 2016 17:05:29 +0200 Subject: [PATCH 606/702] Create function for crreateMappedAccountHolder (single point of access for each persistance operation) #84 --- .../code/bankconnectors/KafkaMappedConnector.scala | 12 ++---------- .../scala/code/bankconnectors/LocalConnector.scala | 6 +----- .../code/bankconnectors/LocalMappedConnector.scala | 6 +----- .../model/dataAccess/MappedAccountHolder.scala | 14 ++++++++++++++ src/main/scala/code/sandbox/OBPDataImport.scala | 7 ++----- ...TestConnectorSetupWithStandardPermissions.scala | 5 +---- 6 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index faeeecc3e..bbcafaedb 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -87,11 +87,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable apiUserOwner match { case Some(o) => { if ( ! accountOwnerExists(o, account)) { - val holder = MappedAccountHolder.create - .user(o) - .accountBankPermalink(account.bank) - .accountPermalink(account.id).saveMe - logger.info(s"------------> created mappedUserHolder ${holder}") + MappedAccountHolder.createMappedAccountHolder(o.apiId.value, account.bank, account.id, "KafkaMappedConnector") } } case None => { @@ -669,11 +665,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable //sets a user as an account owner/holder override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = { - MappedAccountHolder.create - .accountBankPermalink(bankAccountUID.bankId.value) - .accountPermalink(bankAccountUID.accountId.value) - .user(user.apiId.value) - .save + MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankAccountUID.accountId.value, bankAccountUID.bankId.value) } private def createAccountIfNotExisting(bankId: BankId, accountID: AccountId, accountNumber: String, diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 307251111..ce89ee5b9 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -416,11 +416,7 @@ private object LocalConnector extends Connector with Loggable { //sets a user as an account owner/holder override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = { - MappedAccountHolder.create - .accountBankPermalink(bankAccountUID.bankId.value) - .accountPermalink(bankAccountUID.accountId.value) - .user(user.apiId.value) - .save + MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankAccountUID.bankId.value, bankAccountUID.accountId.value) } //for sandbox use -> allows us to check if we can generate a new test account with the given number diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 8cf580b30..e5fb67950 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -420,11 +420,7 @@ object LocalMappedConnector extends Connector with Loggable { //sets a user as an account owner/holder override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = { - MappedAccountHolder.create - .accountBankPermalink(bankAccountUID.bankId.value) - .accountPermalink(bankAccountUID.accountId.value) - .user(user.apiId.value) - .save + MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankAccountUID.bankId.value, bankAccountUID.accountId.value) } private def createAccountIfNotExisting(bankId: BankId, accountId: AccountId, accountNumber: String, diff --git a/src/main/scala/code/model/dataAccess/MappedAccountHolder.scala b/src/main/scala/code/model/dataAccess/MappedAccountHolder.scala index a570e0ec8..41bb05f10 100644 --- a/src/main/scala/code/model/dataAccess/MappedAccountHolder.scala +++ b/src/main/scala/code/model/dataAccess/MappedAccountHolder.scala @@ -1,6 +1,7 @@ package code.model.dataAccess import net.liftweb.mapper._ +import net.liftweb.common._ class MappedAccountHolder extends LongKeyedMapper[MappedAccountHolder] with IdPK { @@ -14,5 +15,18 @@ class MappedAccountHolder extends LongKeyedMapper[MappedAccountHolder] with IdPK } object MappedAccountHolder extends MappedAccountHolder with LongKeyedMetaMapper[MappedAccountHolder] { + + private val logger = Logger(classOf[MappedAccountHolder]) + override def dbIndexes = Index(accountBankPermalink, accountPermalink) :: Nil + + def createMappedAccountHolder(userId: Long, bankId: String, accountId: String, source: String = "MappedAccountHolder"): Boolean = { + val holder = MappedAccountHolder.create + .accountBankPermalink(bankId) + .accountPermalink(accountId) + .user(userId) + .saveMe + if(source != "MappedAccountHolder") logger.info(s"------------> created mappedUserHolder ${holder} at ${source}") + if(holder.saved_?) true else false + } } diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 03030c65a..355377199 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -159,15 +159,12 @@ trait OBPDataImport extends Loggable { * * TODO: this only works after createdUsers have been saved (and thus an APIUser has been created */ - protected def setAccountOwner(owner : AccountOwnerEmail, account: BankAccount, createdUsers: List[APIUser]) : Unit = { + protected def setAccountOwner(owner : AccountOwnerEmail, account: BankAccount, createdUsers: List[APIUser]): AnyVal = { val apiUserOwner = createdUsers.find(user => owner == user.emailAddress) apiUserOwner match { case Some(o) => { - MappedAccountHolder.create - .user(o) - .accountBankPermalink(account.bankId.value) - .accountPermalink(account.accountId.value).save + MappedAccountHolder.createMappedAccountHolder(o.apiId.value, account.bankId.value, account.accountId.value, "OBPDataImport") } case None => { //This shouldn't happen as OBPUser should generate the APIUsers when saved diff --git a/src/test/scala/code/api/TestConnectorSetupWithStandardPermissions.scala b/src/test/scala/code/api/TestConnectorSetupWithStandardPermissions.scala index 686432dfa..dc43ba638 100644 --- a/src/test/scala/code/api/TestConnectorSetupWithStandardPermissions.scala +++ b/src/test/scala/code/api/TestConnectorSetupWithStandardPermissions.scala @@ -14,10 +14,7 @@ import net.liftweb.util.Helpers._ trait TestConnectorSetupWithStandardPermissions extends TestConnectorSetup { override protected def setAccountHolder(user: User, bankId : BankId, accountId : AccountId) = { - MappedAccountHolder.create. - user(user.apiId.value). - accountBankPermalink(bankId.value). - accountPermalink(accountId.value).save + MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankId.value, accountId.value, "TestConnectorSetupWithStandardPermissions") } override protected def grantAccessToAllExistingViews(user : User) = { From 9a86d73dbbe12db766ac64acf9e914dc63c5bfd2 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 8 Jul 2016 19:08:14 +0200 Subject: [PATCH 607/702] Code cleanup --- src/main/scala/code/bankconnectors/KafkaMappedConnector.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index faeeecc3e..5c2a2bae0 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -971,7 +971,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable nationalIdentifier = "", swift_bic = None, iban = None, - number = c.account_number.getOrElse("").substring(0,30), + number = c.account_number.getOrElse(""), bankName = "", kind = "", originalPartyBankId = BankId(o.bankId.value), From 61a3b11896a033f6e08c0e50d70037fcf9909102 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 11 Jul 2016 10:37:28 +0200 Subject: [PATCH 608/702] This closes #86 - Add roles and guards for addSocialMediaHandle getSocialMediaHandles --- src/main/scala/code/api/util/ApiRole.scala | 4 ++++ src/main/scala/code/api/v2_0_0/APIMethods200.scala | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala index 5ec24665c..eb776a49b 100644 --- a/src/main/scala/code/api/util/ApiRole.scala +++ b/src/main/scala/code/api/util/ApiRole.scala @@ -14,6 +14,8 @@ object ApiRole { case object CanGetAnyUser extends ApiRole case object IsHackathonDeveloper extends ApiRole case object CanCreateAnyTransactionRequest extends ApiRole + case object CanAddSocialMediaHandle extends ApiRole + case object CanGetSocialMediaHandles extends ApiRole def valueOf(value: String): ApiRole = value match { case "CanSearchAllTransactions" => CanSearchAllTransactions @@ -26,6 +28,8 @@ object ApiRole { case "CanGetAnyUser" => CanGetAnyUser case "IsHackathonDeveloper" => IsHackathonDeveloper case "CanCreateAnyTransactionRequest" => CanCreateAnyTransactionRequest + case "CanAddSocialMediaHandle" => CanAddSocialMediaHandle + case "CanGetSocialMediaHandles" => CanGetSocialMediaHandles case _ => throw new IllegalArgumentException() } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index c2ad20463..18ffb7dfa 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -502,7 +502,7 @@ trait APIMethods200 { apiVersion, "getSocialMedia", "GET", - "/customers/CUSTOMER_NUMBER/social_media_handles", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media_handles", "Get social media handles for a customer", """Get social media handles for a customer. | @@ -516,10 +516,12 @@ trait APIMethods200 { List(apiTagCustomer, apiTagKyc)) lazy val getSocialMediaHandles : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "social_media_handles" :: Nil JsonGet _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "social_media_handles" :: Nil JsonGet _ => { user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + CanGetSocialMediaHandles <- booleanToBox(hasEntitlement(bank.bankId.value, u.userId, CanGetSocialMediaHandles), s"$CanGetSocialMediaHandles entitlement required") cNumber <- tryo(customerNumber) ?~! {ErrorMessages.CustomerNotFound} } yield { val kycSocialMedias = SocialMediaHandle.socialMediaHandleProvider.vend.getSocialMedias(cNumber) @@ -729,6 +731,7 @@ trait APIMethods200 { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[SocialMediaJSON]} ?~! ErrorMessages.InvalidJsonFormat bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + CanAddSocialMediaHandle <- booleanToBox(hasEntitlement(bank.bankId.value, u.userId, CanAddSocialMediaHandle), s"$CanAddSocialMediaHandle entitlement required") customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound kycSocialMediaCreated <- booleanToBox( SocialMediaHandle.socialMediaHandleProvider.vend.addSocialMedias( From 234ce374f7c6a1cde2440e513bcd934f63b3d222 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 11 Jul 2016 12:45:15 +0200 Subject: [PATCH 609/702] Code cleanup --- src/main/scala/code/bankconnectors/KafkaMappedConnector.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index aca51abfc..ba5df2b72 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -959,7 +959,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable def createOtherBankAccount(c: KafkaInboundTransactionCounterparty, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { new OtherBankAccount( id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), - label = c.name.getOrElse("") + " " + c.account_number.getOrElse(""), + label = c.account_number.getOrElse(c.name.getOrElse("")), nationalIdentifier = "", swift_bic = None, iban = None, From 5c20de94d08107ee1f64db651aa90dce7ab61f64 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 11 Jul 2016 13:04:14 +0200 Subject: [PATCH 610/702] This closes #87 - All GET requests that use customer_number should use customer_id instead --- .../scala/code/api/v2_0_0/APIMethods200.scala | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 18ffb7dfa..4cb015e0a 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -368,7 +368,7 @@ trait APIMethods200 { apiVersion, "getKycDocuments", "GET", - "/customers/CUSTOMER_NUMBER/kyc_documents", + "/customers/CUSTOMER_ID/kyc_documents", "Get KYC Documents for Customer", """Get KYC (know your customer) documents for a customer |Get a list of documents that affirm the identity of the customer @@ -383,13 +383,13 @@ trait APIMethods200 { List(apiTagCustomer, apiTagKyc)) lazy val getKycDocuments : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_documents" :: Nil JsonGet _ => { + case "customers" :: customerId :: "kyc_documents" :: Nil JsonGet _ => { user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn - cNumber <- tryo(customerNumber) ?~! {ErrorMessages.CustomerNotFound} + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId } yield { - val kycDocuments = KycDocuments.kycDocumentProvider.vend.getKycDocuments(cNumber) + val kycDocuments = KycDocuments.kycDocumentProvider.vend.getKycDocuments(customer.number) val json = JSONFactory200.createKycDocumentsJSON(kycDocuments) successJsonResponse(Extraction.decompose(json)) } @@ -403,7 +403,7 @@ trait APIMethods200 { apiVersion, "getKycMedia", "GET", - "/customers/CUSTOMER_NUMBER/kyc_media", + "/customers/CUSTOMER_ID/kyc_media", "Get KYC Media for a customer", """Get KYC media (scans, pictures, videos) that affirms the identity of the customer. | @@ -417,13 +417,13 @@ trait APIMethods200 { List(apiTagCustomer, apiTagKyc)) lazy val getKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_media" :: Nil JsonGet _ => { + case "customers" :: customerId :: "kyc_media" :: Nil JsonGet _ => { user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn - cNumber <- tryo(customerNumber) ?~! {ErrorMessages.CustomerNotFound} + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId } yield { - val kycMedias = KycMedias.kycMediaProvider.vend.getKycMedias(cNumber) + val kycMedias = KycMedias.kycMediaProvider.vend.getKycMedias(customer.number) val json = JSONFactory200.createKycMediasJSON(kycMedias) successJsonResponse(Extraction.decompose(json)) } @@ -436,7 +436,7 @@ trait APIMethods200 { apiVersion, "getKycChecks", "GET", - "/customers/CUSTOMER_NUMBER/kyc_checks", + "/customers/CUSTOMER_ID/kyc_checks", "Get KYC Checks for current Customer", """Get KYC checks for the logged in customer |Messages sent to the currently authenticated user. @@ -451,13 +451,13 @@ trait APIMethods200 { List(apiTagCustomer, apiTagKyc)) lazy val getKycChecks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_checks" :: Nil JsonGet _ => { + case "customers" :: customerId :: "kyc_checks" :: Nil JsonGet _ => { user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn - cNumber <- tryo(customerNumber) ?~! {ErrorMessages.CustomerNotFound} + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId } yield { - val kycChecks = KycChecks.kycCheckProvider.vend.getKycChecks(cNumber) + val kycChecks = KycChecks.kycCheckProvider.vend.getKycChecks(customer.number) val json = JSONFactory200.createKycChecksJSON(kycChecks) successJsonResponse(Extraction.decompose(json)) } @@ -469,7 +469,7 @@ trait APIMethods200 { apiVersion, "getKycStatuses", "GET", - "/customers/CUSTOMER_NUMBER/kyc_statuses", + "/customers/CUSTOMER_ID/kyc_statuses", "Get the KYC statuses for a customer", """Get the KYC statuses for a customer over time | @@ -483,13 +483,13 @@ trait APIMethods200 { List(apiTagCustomer, apiTagKyc)) lazy val getKycStatuses : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "customers" :: customerNumber :: "kyc_statuses" :: Nil JsonGet _ => { + case "customers" :: customerId :: "kyc_statuses" :: Nil JsonGet _ => { user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn - cNumber <- tryo(customerNumber) ?~! {ErrorMessages.CustomerNotFound} + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId } yield { - val kycStatuses = KycStatuses.kycStatusProvider.vend.getKycStatuses(cNumber) + val kycStatuses = KycStatuses.kycStatusProvider.vend.getKycStatuses(customer.number) val json = JSONFactory200.createKycStatusesJSON(kycStatuses) successJsonResponse(Extraction.decompose(json)) } @@ -502,7 +502,7 @@ trait APIMethods200 { apiVersion, "getSocialMedia", "GET", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media_handles", + "/banks/BANK_ID/customers/CUSTOMER_ID/social_media_handles", "Get social media handles for a customer", """Get social media handles for a customer. | @@ -516,15 +516,15 @@ trait APIMethods200 { List(apiTagCustomer, apiTagKyc)) lazy val getSocialMediaHandles : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "social_media_handles" :: Nil JsonGet _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerId :: "social_media_handles" :: Nil JsonGet _ => { user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} CanGetSocialMediaHandles <- booleanToBox(hasEntitlement(bank.bankId.value, u.userId, CanGetSocialMediaHandles), s"$CanGetSocialMediaHandles entitlement required") - cNumber <- tryo(customerNumber) ?~! {ErrorMessages.CustomerNotFound} + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId } yield { - val kycSocialMedias = SocialMediaHandle.socialMediaHandleProvider.vend.getSocialMedias(cNumber) + val kycSocialMedias = SocialMediaHandle.socialMediaHandleProvider.vend.getSocialMedias(customer.number) val json = JSONFactory200.createSocialMediasJSON(kycSocialMedias) successJsonResponse(Extraction.decompose(json)) } From 6e9c0383e53f51bb591701a12e637b3f50c7136c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 12 Jul 2016 09:32:03 +0200 Subject: [PATCH 611/702] addSocialMediaHandle url now matches its get --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index c2ad20463..cff72e15b 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -709,7 +709,7 @@ trait APIMethods200 { apiVersion, "addSocialMediaHandle", "POST", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media", + "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media_handles", "Add Social Media Handle", "Add a social media handle for the customer specified by CUSTOMER_NUMBER.", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), @@ -722,7 +722,7 @@ trait APIMethods200 { ) lazy val addSocialMediaHandle : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "social_media" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "social_media_handles" :: Nil JsonPost json -> _ => { // customerNumber is in url and duplicated in postedData. remove from that? user => { for { From 07a4cb4d7356f0fec323ed29d666f1c7b4a5c7af Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 12 Jul 2016 09:32:19 +0200 Subject: [PATCH 612/702] Add TODO on Bank --- src/main/scala/code/model/BankingData.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index d27b23541..5fffa4a97 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -138,6 +138,9 @@ trait Bank { def logoUrl : String def websiteUrl : String + // TODO Add Group ? + + //SWIFT BIC banking code (globally unique) def swiftBic: String From de806ac880fbf3f89c4963820f0ff41f597a7f1d Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 12 Jul 2016 11:11:53 +0200 Subject: [PATCH 613/702] Removed UUID field from Account/MappedAccount. Removed CashApi --- src/main/scala/bootstrap/liftweb/Boot.scala | 2 - .../scala/code/bankconnectors/Connector.scala | 6 - .../bankconnectors/KafkaMappedConnector.scala | 53 ---- .../code/bankconnectors/LocalConnector.scala | 65 ----- .../bankconnectors/LocalMappedConnector.scala | 59 ----- src/main/scala/code/model/BankingData.scala | 3 - .../scala/code/model/dataAccess/Account.scala | 2 - .../model/dataAccess/MappedBankAccount.scala | 7 +- src/main/scala/code/tesobe/CashAPI.scala | 71 ------ .../scala/code/util/MappedAccountNumber.scala | 2 +- .../code/api/v1_3_0/PhysicalCardsTest.scala | 6 - src/test/scala/code/tesobe/CashAPITest.scala | 232 ------------------ 12 files changed, 3 insertions(+), 505 deletions(-) delete mode 100644 src/test/scala/code/tesobe/CashAPITest.scala diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 0ee364942..a305f2d7b 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -62,7 +62,6 @@ import code.model.dataAccess._ import code.products.MappedProduct import code.transaction_types.MappedTransactionType import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} -import code.tesobe.CashAccountAPI import code.transactionrequests.MappedTransactionRequest import code.usercustomerlinks.MappedUserCustomerLink import net.liftweb.common._ @@ -212,7 +211,6 @@ class Boot extends Loggable{ LiftRules.statelessDispatch.append(AccountsAPI) // add other apis - LiftRules.statelessDispatch.append(CashAccountAPI) LiftRules.statelessDispatch.append(BankMockAPI) diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index 01703cc61..38dd1311e 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -461,12 +461,6 @@ trait Connector { //remove an account and associated transactions def removeAccount(bankId: BankId, accountId: AccountId) : Boolean - //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountId - def getAccountByUUID(uuid : String) : Box[AccountType] - - //cash api requires a call to add a new transaction and update the account balance - def addCashTransactionAndUpdateBalance(account : AccountType, cashTransaction : CashTransaction) - //used by transaction import api call to check for duplicates //the implementation is responsible for dealing with the amount as a string diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index ba5df2b72..96ea4614d 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -696,58 +696,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable Cash api */ - //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountID - override def getAccountByUUID(uuid: String): Box[AccountType] = { - getBankAccount(null, AccountId(uuid)) - } - - //cash api requires a call to add a new transaction and update the account balance - override def addCashTransactionAndUpdateBalance(account: AccountType, cashTransaction: CashTransaction): Unit = { - - val currency = account.currency - val currencyDecimalPlaces = Helper.currencyDecimalPlaces(currency) - - //not ideal to have to convert it this way - def doubleToSmallestCurrencyUnits(x : Double) : Long = { - (x * math.pow(10, currencyDecimalPlaces)).toLong - } - - //can't forget to set the sign of the amount cashed on kind being "in" or "out" - //we just assume if it's not "in", then it's "out" - val amountInSmallestCurrencyUnits = { - if(cashTransaction.kind == "in") doubleToSmallestCurrencyUnits(cashTransaction.amount) - else doubleToSmallestCurrencyUnits(-1 * cashTransaction.amount) - } - - val currentBalanceInSmallestCurrencyUnits = account.balance - val newBalanceInSmallestCurrencyUnits = currentBalanceInSmallestCurrencyUnits + amountInSmallestCurrencyUnits - - //create transaction - val transactionCreated = MappedTransaction.create - .bank(account.bankId.value) - .account(account.accountId.value) - .transactionType("cash") - .amount(amountInSmallestCurrencyUnits) - .newAccountBalance(newBalanceInSmallestCurrencyUnits.toLong) - .currency(account.currency) - .tStartDate(cashTransaction.date) - .tFinishDate(cashTransaction.date) - .description(cashTransaction.label) - .counterpartyAccountHolder(cashTransaction.otherParty) - .counterpartyAccountKind("cash") - .save - - if(!transactionCreated) { - logger.warn("Failed to save cash transaction") - } else { - //update account - val accountUpdated = false //account.balance = newBalanceInSmallestCurrencyUnits - - if(!accountUpdated) - logger.warn("Failed to update account balance after new cash transaction") - } - } - /* End of cash api */ @@ -973,7 +921,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } case class KafkaBankAccount(r: KafkaInboundAccount) extends BankAccount { - def uuid : String = r.id def accountId : AccountId = AccountId(r.id) def accountType : String = r.`type` def balance : BigDecimal = BigDecimal(r.balance.amount) diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index ce89ee5b9..7c9acb948 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -459,71 +459,6 @@ private object LocalConnector extends Connector with Loggable { } - //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountId - override def getAccountByUUID(uuid: String): Box[AccountType] = { - Account.find(uuid) - } - - //cash api requires a call to add a new transaction and update the account balance - override def addCashTransactionAndUpdateBalance(account: AccountType, cashTransaction: CashTransaction): Unit = { - val thisAccountBank = OBPBank.createRecord. - IBAN(account.iban.getOrElse("")). - national_identifier(account.nationalIdentifier). - name(account.bankName) - - val thisAccount = OBPAccount.createRecord. - holder(account.holder.get). - number(account.number). - kind(account.kind.get). - bank(thisAccountBank) - - val otherAccountBank = OBPBank.createRecord. - IBAN(""). - national_identifier(""). - name("") - - val otherAccount = OBPAccount.createRecord. - holder(cashTransaction.otherParty). - number(""). - kind(""). - bank(otherAccountBank) - - val amount : BigDecimal = { - if(cashTransaction.kind == "in") - BigDecimal(cashTransaction.amount).setScale(2,RoundingMode.HALF_UP).abs - else - BigDecimal((cashTransaction.amount * (-1) )).setScale(2,RoundingMode.HALF_UP) - } - - val newBalance : OBPBalance = OBPBalance.createRecord. - currency(account.currency). - amount(account.balance + amount) - - val newValue : OBPValue = OBPValue.createRecord. - currency(account.currency). - amount(amount) - - val details = OBPDetails.createRecord. - kind("cash"). - posted(cashTransaction.date). - other_data(cashTransaction.otherInformation). - new_balance(newBalance). - value(newValue). - completed(cashTransaction.date). - label(cashTransaction.label) - - val transaction = OBPTransaction.createRecord. - this_account(thisAccount). - other_account(otherAccount). - details(details) - - val env = OBPEnvelope.createRecord. - obp_transaction(transaction) - account.accountBalance(account.balance + amount).accountLastUpdate(now) - account.save - env.save - } - //used by transaction import api call to check for duplicates override def getMatchingTransactionCount(bankNationalIdentifier : String, accountNumber : String, amount: String, completed: Date, otherAccountHolder: String): Int = { diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index e5fb67950..f4abe7896 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -445,65 +445,6 @@ object LocalMappedConnector extends Connector with Loggable { End of bank account creation */ - /* - Cash api - */ - - //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountId - override def getAccountByUUID(uuid: String): Box[AccountType] = { - MappedBankAccount.find(By(MappedBankAccount.accUUID, uuid)) - } - - //cash api requires a call to add a new transaction and update the account balance - override def addCashTransactionAndUpdateBalance(account: AccountType, cashTransaction: CashTransaction): Unit = { - - val currency = account.currency - val currencyDecimalPlaces = Helper.currencyDecimalPlaces(currency) - - //not ideal to have to convert it this way - def doubleToSmallestCurrencyUnits(x : Double) : Long = { - (x * math.pow(10, currencyDecimalPlaces)).toLong - } - - //can't forget to set the sign of the amount cashed on kind being "in" or "out" - //we just assume if it's not "in", then it's "out" - val amountInSmallestCurrencyUnits = { - if(cashTransaction.kind == "in") doubleToSmallestCurrencyUnits(cashTransaction.amount) - else doubleToSmallestCurrencyUnits(-1 * cashTransaction.amount) - } - - val currentBalanceInSmallestCurrencyUnits = account.accountBalance.get - val newBalanceInSmallestCurrencyUnits = currentBalanceInSmallestCurrencyUnits + amountInSmallestCurrencyUnits - - //create transaction - val transactionCreated = MappedTransaction.create - .bank(account.bankId.value) - .account(account.accountId.value) - .transactionType("cash") - .amount(amountInSmallestCurrencyUnits) - .newAccountBalance(newBalanceInSmallestCurrencyUnits) - .currency(account.currency) - .tStartDate(cashTransaction.date) - .tFinishDate(cashTransaction.date) - .description(cashTransaction.label) - .counterpartyAccountHolder(cashTransaction.otherParty) - .counterpartyAccountKind("cash") - .save - - if(!transactionCreated) { - logger.warn("Failed to save cash transaction") - } else { - //update account - val accountUpdated = account.accountBalance(newBalanceInSmallestCurrencyUnits).save() - - if(!accountUpdated) - logger.warn("Failed to update account balance after new cash transaction") - } - } - - /* - End of cash api - */ /* Transaction importer api diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index d27b23541..b37ad7344 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -229,9 +229,6 @@ trait BankAccount { @transient protected val log = Logger(this.getClass) - @deprecated - def uuid : String - def accountId : AccountId def accountType : String // (stored in the field "kind" on Mapper) //def productCode : String // TODO Add this shorter code. diff --git a/src/main/scala/code/model/dataAccess/Account.scala b/src/main/scala/code/model/dataAccess/Account.scala index 5f504f30a..1d519c05e 100644 --- a/src/main/scala/code/model/dataAccess/Account.scala +++ b/src/main/scala/code/model/dataAccess/Account.scala @@ -149,8 +149,6 @@ class Account extends BankAccount with MongoRecord[Account] with ObjectIdPk[Acco } } - override def uuid: String = id.get.toString - override def bankId: BankId = { bankID.obj match { case Full(bank) => BankId(bank.permalink.get) diff --git a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala index 6cd46ccc5..9ea05fa59 100644 --- a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala +++ b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala @@ -1,8 +1,9 @@ package code.model.dataAccess import java.util.Date + import code.model._ -import code.util.{MappedAccountNumber, MappedUUID, Helper} +import code.util.{Helper, MappedAccountNumber} import net.liftweb.mapper._ class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccount] with IdPK with CreatedUpdated { @@ -19,9 +20,6 @@ class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccou @deprecated object holder extends MappedString(this, 100) - @deprecated - object accUUID extends MappedUUID(this) - //this is the smallest unit of currency! e.g. cents, yen, pence, øre, etc. object accountBalance extends MappedLong(this) @@ -35,7 +33,6 @@ class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccou //the last time this account was updated via hbci object accountLastUpdate extends MappedDateTime(this) - override def uuid = accUUID.get override def accountId: AccountId = AccountId(theAccountId.get) override def iban: Option[String] = { val i = accountIban.get diff --git a/src/main/scala/code/tesobe/CashAPI.scala b/src/main/scala/code/tesobe/CashAPI.scala index 1fd178939..f03802b7b 100644 --- a/src/main/scala/code/tesobe/CashAPI.scala +++ b/src/main/scala/code/tesobe/CashAPI.scala @@ -1,19 +1,6 @@ package code.tesobe import java.util.Date -import code.bankconnectors.Connector -import code.model.dataAccess._ -import net.liftweb.common.{Full, Box, Loggable} -import net.liftweb.http.{JsonResponse, S} -import net.liftweb.http.rest.RestHelper -import net.liftweb.json.{JsonAST, Extraction} -import net.liftweb.json.JsonAST.JValue -import net.liftweb.util.Props -import net.liftweb.util.Helpers._ -import net.liftweb.json.pretty -import net.liftweb.json.JsonDSL._ - -import scala.math.BigDecimal.RoundingMode case class CashTransaction( otherParty : String, @@ -28,62 +15,4 @@ case class ErrorMessage(error: String) case class SuccessMessage(success: String) -object CashAccountAPI extends RestHelper with Loggable { - implicit def errorToJson(error: ErrorMessage): JValue = Extraction.decompose(error) - implicit def successToJson(msg: SuccessMessage): JValue = Extraction.decompose(msg) - - def isValidKey : Boolean = { - val sentKey : Box[String] = - for{ - req <- S.request - sentKey <- req.header("cashApplicationKey") - } yield sentKey - val localKey : Box[String] = Props.get("cashApplicationKey") - localKey == sentKey - } - - lazy val invalidCashTransactionJsonFormatError = { - val error = "Post data must be in the format: \n" + - pretty( - JsonAST.render( - Extraction.decompose( - CashTransaction( - otherParty = "other party", - date = new Date, - amount = 1231.12, - kind = "in / out", - label = "what is this transaction for", - otherInformation = "more info" - )))) - JsonResponse(ErrorMessage(error), Nil, Nil, 400) - } - - serve("obp" / "v1.0" prefix { - - case "cash-accounts" :: uuid :: "transactions" :: Nil JsonPost json -> _ => { - - val connector = Connector.connector.vend - - if(isValidKey) { - connector.getAccountByUUID(uuid) match { - case Full(account) => - tryo { json.extract[CashTransaction] } match { - case Full(cashTransaction) => - connector.addCashTransactionAndUpdateBalance(account, cashTransaction) - JsonResponse(SuccessMessage("transaction successfully added"), Nil, Nil, 200) - case _ => - invalidCashTransactionJsonFormatError - } - case _ => - JsonResponse(ErrorMessage("Account " + uuid + " not found" ), Nil, Nil, 400) - } - - } else { - JsonResponse(ErrorMessage("No key found or wrong key"), Nil, Nil, 401) - } - - } - - }) -} diff --git a/src/main/scala/code/util/MappedAccountNumber.scala b/src/main/scala/code/util/MappedAccountNumber.scala index 34eccb898..1d9b31008 100644 --- a/src/main/scala/code/util/MappedAccountNumber.scala +++ b/src/main/scala/code/util/MappedAccountNumber.scala @@ -6,5 +6,5 @@ import net.liftweb.mapper.{Mapper, MappedString} class MappedAccountNumber [T <: Mapper[T]] (override val fieldOwner : T) extends MappedString(fieldOwner, MappedAccountNumber.MaxLength) object MappedAccountNumber { - val MaxLength = 30 + val MaxLength = 128 } \ No newline at end of file diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index 9c0d89c95..c11b10034 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -121,12 +121,6 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers { accountNumber: String, currency: String, initialBalance: BigDecimal, accountHolderName: String): Box[AccountType] = ??? - //cash api requires getting an account via a uuid: for legacy reasons it does not use bankId + accountId - override def getAccountByUUID(uuid: String): Box[PhysicalCardsTest.this.MockedCardConnector.AccountType] = ??? - - //cash api requires a call to add a new transaction and update the account balance - override def addCashTransactionAndUpdateBalance(account: PhysicalCardsTest.this.MockedCardConnector.AccountType, cashTransaction: CashTransaction): Unit = ??? - //used by transaction import api call to check for duplicates override def getMatchingTransactionCount(bankNationalIdentifier : String, accountNumber : String, amount: String, completed: Date, otherAccountHolder: String): Int = ??? //used by transaction import api diff --git a/src/test/scala/code/tesobe/CashAPITest.scala b/src/test/scala/code/tesobe/CashAPITest.scala deleted file mode 100644 index 1f299bab5..000000000 --- a/src/test/scala/code/tesobe/CashAPITest.scala +++ /dev/null @@ -1,232 +0,0 @@ -package code.tesobe - -import java.util.{Date, UUID} - -import code.api.{APIResponse, DefaultConnectorTestSetup, ServerSetup} -import code.bankconnectors.Connector -import code.model.AccountId -import net.liftweb.common.Loggable -import net.liftweb.json.Serialization.write -import net.liftweb.util.Props - -/** - * The cash api isn't very well designed and is used only for internal projects. If we want to - * expand this, a rewrite would be best, though we would need to update the applications using this api. - * - * API issues: - * -CashTransaction.kind is used as a switch for if the transaction value should be positive (incoming) or - * negative (outgoing) by checking if it's value is "in" or not - * -Despite this parameter, there are no checks to verify CashTransaction.value should be positive (to avoid - * a situation where value is negative but kind is outgoing) - * -Uses a double value for transaction value, instead of String/BigDecimal - * - */ -class CashAPITest extends ServerSetup with Loggable with DefaultConnectorTestSetup { - - override def beforeEach() = { - super.beforeEach() - wipeTestData() - } - - override def afterEach() = { - super.afterEach() - wipeTestData() - } - - def fixture() = new { - lazy val bank = createBank("test-bank") - lazy val account = createAccount(bank.bankId, AccountId("some-account-id"), "EUR") - lazy val incomingTransactionData = CashTransaction( - otherParty = "foo", - date = new Date(), - amount = 12.33, - kind = "in", - label = "some label", - otherInformation = "some more info" - ) - lazy val outgoingTransactionData = incomingTransactionData.copy(kind = "out") - } - - def addCashTransaction(accountUUID : String, data : String, secretKey : Option[String]) : APIResponse = { - - val baseReq = (baseRequest / "obp" / "v1.0" / "cash-accounts" / accountUUID / "transactions").POST - - val req = secretKey match { - case Some(key) => baseReq <:< Map(CashKeyParam -> key) //the key goes in a header - case None => baseReq - } - - makePostRequest(req, data) - } - - def addCashTransaction(accountUUID : String, data : CashTransaction, secretKey : Option[String]) : APIResponse = { - addCashTransaction(accountUUID, write(data), secretKey) - } - - def checkSameDate(d1 : Date, d2 : Date) = { - //we want to check they are the same date in the api output format - //since the api discards some small fractions of a second, a direct date comparison will fail - formats.dateFormat.format(d1) should equal(formats.dateFormat.format(d1)) - } - - val CashKeyParam = "cashApplicationKey" - val validKey = Props.get(CashKeyParam).openOr("false") //.openOrThrowException("Props key CashKeyParam not found") - - if(validKey != "false") { - feature("Adding cash transactions to accounts") { - - scenario("Attempting to add a transaction to an existing account using an incorrect secret key") { - val f = fixture() - Given("An invalid key") - val invalidKey = validKey + "1" - - And("An account with no existing transactions") - Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).isDefined should equal(true) - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val response = addCashTransaction(f.account.uuid, f.incomingTransactionData, Some(invalidKey)) - - Then("We should get a 401 not authorized") - response.code should equal(401) - - And("No transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(0) - } - - scenario("Attempting to add a transaction to an existing account without using a secret key ") { - val f = fixture() - Given("An account with no existing transactions") - Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).isDefined should equal(true) - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val response = addCashTransaction(f.account.uuid, f.incomingTransactionData, None) - - Then("We should get a 401 not authorized") - response.code should equal(401) - - And("No transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(0) - } - - scenario("Attempting to add a transaction to nonexistent account using the correct secret key") { - val f = fixture() - val nonexistentAccountUUID = UUID.randomUUID().toString - - When("We try to add a cash transaction") - val response = addCashTransaction(nonexistentAccountUUID, f.incomingTransactionData, Some(validKey)) - - Then("We should get a 400") - response.code should equal(400) - } - - - scenario("Attempting to add an incoming transaction to an existing account using the correct secret key") { - val f = fixture() - Given("An account with no existing transactions") - val accountBox = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId) - accountBox.isDefined should equal(true) - - val balanceBefore = accountBox.get.balance - - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val tData = f.incomingTransactionData - val response = addCashTransaction(f.account.uuid, tData, Some(validKey)) - - //for some reason this was originally set to 200 instead of 201, but we'll leave it that way to avoid breaking anything - Then("We should get a 200") - response.code should equal(200) - - //TODO: check response body format? - - And("The transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(1) - - val addedTransaction = tsAfter(0) - - And("This transaction should have the appropriate values") - checkSameDate(addedTransaction.finishDate, tData.date) - addedTransaction.description should equal(Some(tData.label)) - addedTransaction.otherAccount.label should equal(tData.otherParty) - - //cash api should always set transaction type to cash - addedTransaction.transactionType should equal("cash") - - //incoming transaction should have positive value - addedTransaction.amount.toDouble should equal(tData.amount) //icky BigDecimal to double conversion here.. - - And("The account should have its balance properly updated") - val balanceAfter = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get.balance - balanceAfter.toDouble should equal(balanceBefore.toDouble + tData.amount) //icky BigDecimal to double conversion here.. - } - - scenario("Attempting to add an outgoing transaction to an existing account using the correct secret key") { - val f = fixture() - Given("An account with no existing transactions") - val accountBox = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId) - accountBox.isDefined should equal(true) - - val balanceBefore = accountBox.get.balance - - val tsBefore = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsBefore.size should equal(0) - - When("We try to add a cash transaction") - val tData = f.outgoingTransactionData - val response = addCashTransaction(f.account.uuid, tData, Some(validKey)) - - //for some reason this was originally set to 200 instead of 201, but we'll leave it that way to avoid breaking anything - Then("We should get a 200") - response.code should equal(200) - - //TODO: check response body format? - - And("The transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(1) - - val addedTransaction = tsAfter(0) - - And("This transaction should have the appropriate values") - checkSameDate(addedTransaction.finishDate, tData.date) - addedTransaction.description should equal(Some(tData.label)) - addedTransaction.otherAccount.label should equal(tData.otherParty) - - //cash api should always set transaction type to cash - addedTransaction.transactionType should equal("cash") - - //outgoing transaction should have negative value - addedTransaction.amount.toDouble should equal(-1 * tData.amount) //icky BigDecimal to double conversion here.. - - And("The account should have its balance properly updated") - val balanceAfter = Connector.connector.vend.getBankAccount(f.account.bankId, f.account.accountId).get.balance - balanceAfter.toDouble should equal(balanceBefore.toDouble - tData.amount) //icky BigDecimal to double conversion here.. - } - - scenario("Sending the wrong kind of json") { - val f = fixture() - - When("We send the wrong json format") - val response = addCashTransaction(f.account.uuid, """{"foo": "bar"}""", Some(validKey)) - - Then("We should get a 400") - response.code should equal(400) - - And("No transaction should be added") - val tsAfter = Connector.connector.vend.getTransactions(f.account.bankId, f.account.accountId).get - tsAfter.size should equal(0) - } - - } - } - -} From a57742aac497b12163ec6a6985d5ad93f893aa57 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 13 Jul 2016 12:33:55 +0200 Subject: [PATCH 614/702] Fixed views, now adds user to existing view permissions --- .../scala/code/bankconnectors/KafkaMappedConnector.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 96ea4614d..9f2bf4508 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -127,14 +127,18 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable acc <- accounts.getOrElse(List.empty) email <- tryo {user.emailAddress} views <- tryo {createSaveableViews(acc, acc.owners.contains(email))} + existing_views <- tryo {Views.views.vend.views(new KafkaBankAccount(acc))} } yield { setAccountOwner(email, acc) views.foreach(_.save()) - //views.map(_.value).filterNot(_.isPublic).foreach(v => { views.map(_.value).foreach(v => { Views.views.vend.addPermission(v.uid, user) logger.info(s"------------> added view ${v.uid} for apiuser ${user} and account ${acc}") }) + existing_views.foreach(v => { + Views.views.vend.addPermission(v.uid, user) + logger.info(s"------------> added apiuser ${user} to view ${v.uid} for account ${acc}") + }) } } From c5b15edf54d7f2e4391506a1cb3cd6454c0430c5 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 13 Jul 2016 12:49:42 +0200 Subject: [PATCH 615/702] Filtering out existing users for the view --- src/main/scala/code/bankconnectors/KafkaMappedConnector.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 9f2bf4508..6adc9eddb 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -135,7 +135,7 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable Views.views.vend.addPermission(v.uid, user) logger.info(s"------------> added view ${v.uid} for apiuser ${user} and account ${acc}") }) - existing_views.foreach(v => { + existing_views.filterNot(_.users.contains(user)).foreach (v => { Views.views.vend.addPermission(v.uid, user) logger.info(s"------------> added apiuser ${user} to view ${v.uid} for account ${acc}") }) From 5c8ebab1786a7b6840efcecc0c69cb6efc60d982 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Wed, 13 Jul 2016 17:08:39 +0200 Subject: [PATCH 616/702] More fixes to account views in kafka connector --- .../bankconnectors/KafkaMappedConnector.scala | 64 ++++++++++--------- .../scala/code/model/dataAccess/OBPUser.scala | 31 ++++----- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 6adc9eddb..06c717ca0 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -3,6 +3,7 @@ package code.bankconnectors import java.text.SimpleDateFormat import java.util.{Date, Locale, UUID} +import code.api.util.ErrorMessages import code.management.ImporterAPI.ImporterTransaction import code.metadata.comments.MappedComment import code.metadata.counterparties.Counterparties @@ -43,31 +44,15 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val cachedUserAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) def getUser( username: String, password: String ): Box[KafkaInboundUser] = { - // Generate random uuid to be used as request-response match id - val reqId: String = UUID.randomUUID().toString - // Send request to Kafka, marked with reqId - // so we can fetch the corresponding response - val argList = Map( "email" -> username.toLowerCase, - "password" -> password ) - implicit val formats = net.liftweb.json.DefaultFormats - - val r = { - cachedUser.getOrElseUpdate( argList.toString, () => process(reqId, "getUser", argList).extract[KafkaInboundValidatedUser]) - } - val recDisplayName = r.display_name - val recEmail = r.email - if (recEmail == username.toLowerCase && recEmail != "Not Found") { - if (recDisplayName == "") { - val user = new KafkaInboundUser( recEmail, password, recEmail) - Full(user) - } - else { - val user = new KafkaInboundUser(recEmail, password, recDisplayName) - Full(user) - } - } else { - // If empty result from Kafka return empty data - Empty + for { + argList <- tryo {Map[String, String]( "email" -> username.toLowerCase, "password" -> password )} + // Generate random uuid to be used as request-response match id + reqId <- tryo {UUID.randomUUID().toString} + u <- tryo{cachedUser.getOrElseUpdate( argList.toString, () => process(reqId, "getUser", argList).extract[KafkaInboundValidatedUser])} + recEmail <- tryo{u.email} ?~ {ErrorMessages.UserNotFoundByEmail} + user <- tryo{new KafkaInboundUser( recEmail, password, recEmail)} + } yield { + user } } @@ -101,12 +86,17 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable for { email <- tryo{user.emailAddress} views <- tryo{createSaveableViews(account, account.owners.contains(email))} + existing_views <- tryo {Views.views.vend.views(new KafkaBankAccount(account))} } yield { views.foreach(_.save()) views.map(_.value).foreach(v => { Views.views.vend.addPermission(v.uid, user) logger.info(s"------------> updated view ${v.uid} for apiuser ${user} and account ${account}") }) + existing_views.filterNot(_.users.contains(user)).foreach (v => { + Views.views.vend.addPermission(v.uid, user) + logger.info(s"------------> added apiuser ${user} to view ${v.uid} for account ${account}") + }) setAccountOwner(email, account) } } @@ -307,11 +297,10 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable for { e <- tryo{OBPUser.getCurrentUserUsername} u <- OBPUser.getApiUserByEmail(e) + } yield { + updateAccountViews(u, r) } - yield { - updateAccountViews(u, r) - } - Full(res) + Full(res) } override def getBankAccounts(accts: List[(BankId, AccountId)]): List[KafkaBankAccount] = { @@ -327,7 +316,16 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val r = { cachedAccounts.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccounts", argList).extract[List[KafkaInboundAccount]]) } - val res = r.map ( t => new KafkaBankAccount(t) ) + val res = r.map { t => + val a = new KafkaBankAccount(t) + for { + e <- tryo{OBPUser.getCurrentUserUsername} + u <- OBPUser.getApiUserByEmail(e) + } yield { + updateAccountViews(u, t) + } + a + } res } @@ -345,6 +343,12 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable cachedAccount.getOrElseUpdate( argList.toString, () => process(reqId, "getBankAccount", argList).extract[KafkaInboundAccount]) } val res = new KafkaBankAccount(r) + for { + e <- tryo{OBPUser.getCurrentUserUsername} + u <- OBPUser.getApiUserByEmail(e) + } yield { + updateAccountViews(u, r) + } Full(res) } diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 5805cfc0f..532db5d3b 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -367,12 +367,9 @@ import net.liftweb.util.Helpers._ case _ => homePage } - logUserIn(user, () => { S.notice(S.?("logged.in")) - preLoginState() - S.redirectTo(redir) }) } @@ -384,26 +381,24 @@ import net.liftweb.util.Helpers._ // If not found locally, try to authenticate user via Kafka, if enabled in props if (Props.get("connector").openOrThrowException("no connector set") == "kafka") { val preLoginState = capturePreLoginState() - val extUser = getUserFromKafka(S.param("username").orNull, S.param("password").orNull) - - if (!extUser.isEmpty) { - val u = APIUser.find(By(APIUser.email, extUser.getOrElse(null).email)).getOrElse(null) - if (u != null) { - KafkaMappedConnector.updateUserAccountViews(u) - } - - logUserIn(extUser.orNull, () => { + val extUser = for { + username_ <- S.param("username") + password_ <- S.param("password") + user_ <- getUserFromKafka(username_, password_) + } yield { + logUserIn(user_, () => { S.notice(S.?("logged.in")) - preLoginState() - S.redirectTo(homePage) }) - } else { - userLoginFailed + for { + u <- APIUser.find(By(APIUser.email, user_.email)) + v <- tryo{KafkaMappedConnector.updateUserAccountViews(u)} + } + user_ } - } else { - userLoginFailed + if (extUser.isEmpty) + userLoginFailed } } } From 7ae76e304c3ceddd84cc71ba86aab21da4d47cf9 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Thu, 14 Jul 2016 00:13:08 +0300 Subject: [PATCH 617/702] Add migrations folder --- src/main/scripts/migrate/migrate_0000001.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/main/scripts/migrate/migrate_0000001.sql diff --git a/src/main/scripts/migrate/migrate_0000001.sql b/src/main/scripts/migrate/migrate_0000001.sql new file mode 100644 index 000000000..0b5365c5b --- /dev/null +++ b/src/main/scripts/migrate/migrate_0000001.sql @@ -0,0 +1,3 @@ +alter table mappedbankaccount alter column accuuid type varchar(80); + +alter table mappedbankaccount alter column accountnumber type varchar(80); \ No newline at end of file From 3c5457d9da1969aac2892ecb2e0f13580cf007fb Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 14 Jul 2016 15:14:16 +0200 Subject: [PATCH 618/702] All GET requests that use customer_number should use customer_id instead #87 --- src/main/scala/code/api/util/APIUtil.scala | 1 + .../scala/code/api/v1_4_0/APIMethods140.scala | 13 ++++--- .../scala/code/api/v2_0_0/APIMethods200.scala | 38 +++++++++---------- .../MappedUserCustomerLink.scala | 5 +++ .../usercustomerlinks/UserCustomerLink.scala | 1 + .../v1_4_0/MappedCustomerMessagesTest.scala | 19 +++++++++- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 7d6458575..4d44a6322 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -89,6 +89,7 @@ object ErrorMessages { val CustomerNumberAlreadyExists = "OBP-30006: Customer Number already exists. Please specify a different value for BANK_ID or CUSTOMER_NUMBER." val CustomerAlreadyExistsForUser = "OBP-30007: The User is already linked to a Customer at BANK_ID" + val CustomerDoNotExistsForUser = "OBP-30008: User is not linked to a Customer at BANK_ID" val MeetingsNotSupported = "OBP-30101: Meetings are not supported on this server." val MeetingApiKeyNotConfigured = "OBP-30102: Meeting provider API Key is not configured." diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index da84316a2..da5eb2fde 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -7,6 +7,7 @@ import code.api.v1_4_0.JSONFactory1_4_0._ import code.bankconnectors.Connector import code.metadata.comments.MappedComment import code.transactionrequests.TransactionRequests.{TransactionRequestBody, TransactionRequestAccount} +import code.usercustomerlinks.UserCustomerLink import net.liftweb.common.{Failure, Loggable, Box, Full} import net.liftweb.http.js.JE.JsRaw import net.liftweb.http.{JsonResponse, Req} @@ -139,9 +140,9 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ apiVersion, "addCustomerMessage", "POST", - "/banks/BANK_ID/customer/CUSTOMER_NUMBER/messages", + "/banks/BANK_ID/customer/CUSTOMER_ID/messages", "Add Customer Message.", - "Add a message for the customer specified by CUSTOMER_NUMBER", + "Add a message for the customer specified by CUSTOMER_ID", // We use Extraction.decompose to convert to json Extraction.decompose(AddCustomerMessageJson("message to send", "from department", "from person")), emptyObjectJson, @@ -153,15 +154,17 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ ) lazy val addCustomerMessage : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customer" :: customerNumber :: "messages" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customer" :: customerId :: "messages" :: Nil JsonPost json -> _ => { user => { for { postedData <- tryo{json.extract[AddCustomerMessageJson]} ?~! "Incorrect json format" bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - customer <- Customer.customerProvider.vend.getUser(bankId, customerNumber) ?~! "Customer not found" + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId + userCustomerLink <- UserCustomerLink.userCustomerLink.vend.getUserCustomerLink(customer.customerId) ?~! ErrorMessages.CustomerDoNotExistsForUser + user <- User.findByUserId(userCustomerLink.userId) ?~! ErrorMessages.UserNotFoundById messageCreated <- booleanToBox( CustomerMessages.customerMessageProvider.vend.addMessage( - customer, bankId, postedData.message, postedData.from_department, postedData.from_person), + user, bankId, postedData.message, postedData.from_department, postedData.from_person), "Server error: could not add message") } yield { successJsonResponse(JsRaw("{}"), 201) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 0e9e32c96..e0145413b 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -540,9 +540,9 @@ trait APIMethods200 { apiVersion, "addKycDocument", "POST", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_documents", + "/banks/BANK_ID/customers/CUSTOMER_ID/kyc_documents", "Add KYC Document.", - "Add a KYC document for the customer specified by CUSTOMER_NUMBER. KYC Documents contain the document type (e.g. passport), place of issue, expiry etc. ", + "Add a KYC document for the customer specified by CUSTOMER_ID. KYC Documents contain the document type (e.g. passport), place of issue, expiry etc. ", Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -555,14 +555,14 @@ trait APIMethods200 { // TODO customerNumber should be in the url but not also in the postedData lazy val addKycDocument : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_documents" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerId :: "kyc_documents" :: Nil JsonPost json -> _ => { // customerNumber is duplicated in postedData. remove from that? user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycDocumentJSON]} ?~! ErrorMessages.InvalidJsonFormat bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId kycDocumentCreated <- booleanToBox( KycDocuments.kycDocumentProvider.vend.addKycDocuments( postedData.id, @@ -585,9 +585,9 @@ trait APIMethods200 { apiVersion, "addKycMedia", "POST", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_media", + "/banks/BANK_ID/customers/CUSTOMER_ID/kyc_media", "Add KYC Media.", - "Add some KYC media for the customer specified by CUSTOMER_NUMBER. KYC Media resources relate to KYC Documents and KYC Checks and contain media urls for scans of passports, utility bills etc.", + "Add some KYC media for the customer specified by CUSTOMER_ID. KYC Media resources relate to KYC Documents and KYC Checks and contain media urls for scans of passports, utility bills etc.", Extraction.decompose(KycMediaJSON("73hyfgayt6ywerwerasd", "1239879", "image", "http://www.example.com/id-docs/123/image.png", exampleDate, "wuwjfuha234678", "98FRd987auhf87jab")), emptyObjectJson, emptyObjectJson :: Nil, @@ -598,14 +598,14 @@ trait APIMethods200 { ) lazy val addKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_media" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerId :: "kyc_media" :: Nil JsonPost json -> _ => { // customerNumber is in url and duplicated in postedData. remove from that? user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycMediaJSON]} ?~! ErrorMessages.InvalidJsonFormat bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId kycDocumentCreated <- booleanToBox( KycMedias.kycMediaProvider.vend.addKycMedias( postedData.id, @@ -628,9 +628,9 @@ trait APIMethods200 { apiVersion, "addKycCheck", "POST", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_check", + "/banks/BANK_ID/customers/CUSTOMER_ID/kyc_check", "Add KYC Check", - "Add a KYC check for the customer specified by CUSTOMER_NUMBER. KYC Checks store details of checks on a customer made by the KYC team, their comments and a satisfied status.", + "Add a KYC check for the customer specified by CUSTOMER_ID. KYC Checks store details of checks on a customer made by the KYC team, their comments and a satisfied status.", Extraction.decompose(KycCheckJSON("98FRd987auhf87jab", "1239879", exampleDate, "online_meeting", "67876", "Simon Redfern", true, "")), emptyObjectJson, emptyObjectJson :: Nil, @@ -641,14 +641,14 @@ trait APIMethods200 { ) lazy val addKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_check" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerId :: "kyc_check" :: Nil JsonPost json -> _ => { // customerNumber is in url and duplicated in postedData. remove from that? user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycCheckJSON]} ?~! ErrorMessages.InvalidJsonFormat bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId kycCheckCreated <- booleanToBox( KycChecks.kycCheckProvider.vend.addKycChecks( postedData.id, @@ -672,9 +672,9 @@ trait APIMethods200 { apiVersion, "addKycStatus", "POST", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_statuses", + "/banks/BANK_ID/customers/CUSTOMER_ID/kyc_statuses", "Add KYC Status", - "Add a kyc_status for the customer specified by CUSTOMER_NUMBER. KYC Status is a timeline of the KYC status of the customer", + "Add a kyc_status for the customer specified by CUSTOMER_ID. KYC Status is a timeline of the KYC status of the customer", Extraction.decompose(KycStatusJSON("8762893876", true, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -685,14 +685,14 @@ trait APIMethods200 { ) lazy val addKycStatus : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_statuses" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerId :: "kyc_statuses" :: Nil JsonPost json -> _ => { // customerNumber is in url and duplicated in postedData. remove from that? user => { for { u <- user ?~! ErrorMessages.UserNotLoggedIn postedData <- tryo{json.extract[KycStatusJSON]} ?~! ErrorMessages.InvalidJsonFormat bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId kycStatusCreated <- booleanToBox( KycStatuses.kycStatusProvider.vend.addKycStatus( postedData.customer_number, @@ -713,7 +713,7 @@ trait APIMethods200 { "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media_handles", "Add Social Media Handle", - "Add a social media handle for the customer specified by CUSTOMER_NUMBER.", + "Add a social media handle for the customer specified by CUSTOMER_ID.", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -724,7 +724,7 @@ trait APIMethods200 { ) lazy val addSocialMediaHandle : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "social_media_handles" :: Nil JsonPost json -> _ => { + case "banks" :: BankId(bankId) :: "customers" :: customerId :: "social_media_handles" :: Nil JsonPost json -> _ => { // customerNumber is in url and duplicated in postedData. remove from that? user => { for { @@ -732,7 +732,7 @@ trait APIMethods200 { postedData <- tryo{json.extract[SocialMediaJSON]} ?~! ErrorMessages.InvalidJsonFormat bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} CanAddSocialMediaHandle <- booleanToBox(hasEntitlement(bank.bankId.value, u.userId, CanAddSocialMediaHandle), s"$CanAddSocialMediaHandle entitlement required") - customer <- Customer.customerProvider.vend.getUser(bankId, postedData.customer_number) ?~! ErrorMessages.CustomerNotFound + customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId kycSocialMediaCreated <- booleanToBox( SocialMediaHandle.socialMediaHandleProvider.vend.addSocialMedias( postedData.customer_number, diff --git a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala index c2c1af586..010d6ffab 100644 --- a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala +++ b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala @@ -39,6 +39,11 @@ class MappedUserCustomerLink extends UserCustomerLink with LongKeyedMapper[Mappe Some(createUserCustomerLink) } + override def getUserCustomerLink(customerId: String): Box[UserCustomerLink] = { + MappedUserCustomerLink.find( + By(MappedUserCustomerLink.mCustomerId, customerId)) + } + override def getUserCustomerLink(userId : String, customerId: String): Box[UserCustomerLink] = { MappedUserCustomerLink.find( By(MappedUserCustomerLink.mUserId, userId), diff --git a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala index 0caaecb6b..d11b5056f 100644 --- a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala +++ b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala @@ -22,6 +22,7 @@ trait UserCustomerLink { def isActive: Boolean def createUserCustomerLink(userId: String, customerId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] + def getUserCustomerLink(customerId: String): Box[UserCustomerLink] def getUserCustomerLink(userId: String, customerId: String): Box[UserCustomerLink] def getUserCustomerLinks: Box[List[UserCustomerLink]] } \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala index 2f7726d04..44cec38be 100644 --- a/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala +++ b/src/test/scala/code/api/v1_4_0/MappedCustomerMessagesTest.scala @@ -7,9 +7,13 @@ import code.api.util.APIUtil import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, AddCustomerMessageJson, CustomerMessagesJson, CustomerJson} import code.customer.{MappedCustomerMessage, MappedCustomer, Customer} import code.model.BankId +import code.usercustomerlinks.{MappedUserCustomerLink} import dispatch._ import code.api.util.APIUtil.OAuth._ +import net.liftweb.common.Box import net.liftweb.json.Serialization.{read, write} +import net.liftweb.mapper.By +import net.liftweb.common.{Full, Empty} //TODO: API test should be independent of CustomerMessages implementation class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { @@ -17,7 +21,7 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { val mockBankId = BankId("testBank1") val mockCustomerNumber = "9393490320" - val mockCustomerId = "uuid-asdfasdfaoiu8u8u8hkjhsf" + val mockCustomerId = "cba6c9ef-73fa-4032-9546-c6f6496b354a" val exampleDateString : String ="22/08/2013" @@ -63,8 +67,19 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers { ) var response = makePostRequest(request, write(customerJson)) + val customer: Box[MappedCustomer] = MappedCustomer.find( + By(MappedCustomer.mBank, mockBankId.value), + By(MappedCustomer.mNumber, mockCustomerNumber) + ) + val customerId = customer match { + case Full(c) => c.customerId + case Empty => "Empty" + case _ => "Failure" + } + MappedUserCustomerLink.createUserCustomerLink(obpuser1.userId, customerId, exampleDate, true) + When("We add a message") - request = (v1_4Request / "banks" / mockBankId.value / "customer" / mockCustomerNumber / "messages").POST <@ user1 + request = (v1_4Request / "banks" / mockBankId.value / "customer" / customerId / "messages").POST <@ user1 val messageJson = AddCustomerMessageJson("some message", "some department", "some person") response = makePostRequest(request, write(messageJson)) From 1a80315b54f3f43b846ca3baf51f2918b07ae42f Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 14 Jul 2016 15:19:02 +0200 Subject: [PATCH 619/702] All GET requests that use customer_number should use customer_id instead #87 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index e0145413b..0bc28a417 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -711,7 +711,7 @@ trait APIMethods200 { apiVersion, "addSocialMediaHandle", "POST", - "/banks/BANK_ID/customers/CUSTOMER_NUMBER/social_media_handles", + "/banks/BANK_ID/customers/CUSTOMER_ID/social_media_handles", "Add Social Media Handle", "Add a social media handle for the customer specified by CUSTOMER_ID.", Extraction.decompose(SocialMediaJSON("8762893876", "twitter", "susan@example.com", exampleDate, exampleDate)), From f81292e1dc34b7f8e095c153296a11fcc3c2432f Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 15 Jul 2016 01:24:38 +0100 Subject: [PATCH 620/702] authenticationRequiredMessage for consistant authentication message --- src/main/scala/code/api/util/APIUtil.scala | 11 ++- .../scala/code/api/v1_2_1/APIMethods121.scala | 95 +++++++++++-------- .../scala/code/api/v1_4_0/APIMethods140.scala | 4 +- .../scala/code/api/v2_0_0/APIMethods200.scala | 58 ++++++----- 4 files changed, 103 insertions(+), 65 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 7d6458575..304f4c591 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -474,10 +474,15 @@ object APIUtil extends Loggable { + + +/* +Returns a string showed to the developer + */ def authenticationRequiredMessage(authRequired: Boolean) : String = - authRequired match { - case true => "Authentication IS required" - case false => "Authentication is NOT required" + authRequired match { + case true => "Authentication is Mandatory" + case false => "Authentication is Optional" } diff --git a/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala index c418ff9a7..fb3e0d4dc 100644 --- a/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -389,7 +389,7 @@ trait APIMethods121 { "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account", "Get account by id.", - """Information returned about an account specified by ACCOUNT_ID as moderated by the view (VIEW_ID): + s"""Information returned about an account specified by ACCOUNT_ID as moderated by the view (VIEW_ID): | |* Number |* Owners @@ -400,7 +400,9 @@ trait APIMethods121 { | |More details about the data moderation by the view [here](#1_2_1-getViewsForBankAccount). | - |OAuth authentication is required if the 'is_public' field in view (VIEW_ID) is not set to `true`.""", + |${authenticationRequiredMessage(false)} + | + |Authentication is required if the 'is_public' field in view (VIEW_ID) is not set to `true`.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -832,9 +834,9 @@ trait APIMethods121 { "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts", "Get counterparties of one account.", - """Returns data about all the other bank accounts that have shared at least one transaction with the ACCOUNT_ID at BANK_ID. - | - |OAuth authentication is required if the view VIEW_ID is not public.""", + s"""Returns data about all the other bank accounts that have shared at least one transaction with the ACCOUNT_ID at BANK_ID. + |${authenticationRequiredMessage(false)} + |Authentication is required if the view VIEW_ID is not public.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -865,9 +867,9 @@ trait APIMethods121 { "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID", "Get counterparty by id.", - """Returns data about one other counterparty (bank account) (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. - | - |OAuth authentication is required if the view is not public.""", + s"""Returns data about one other counterparty (bank account) (OTHER_ACCOUNT_ID) that had shared at least one transaction with ACCOUNT_ID at BANK_ID. + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -933,8 +935,8 @@ trait APIMethods121 { "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias", "Get public alias of other bank account.", - """Returns the public alias of the other account OTHER_ACCOUNT_ID. - | + s"""Returns the public alias of the other account OTHER_ACCOUNT_ID. + |${authenticationRequiredMessage(false)} |OAuth authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, @@ -968,11 +970,12 @@ trait APIMethods121 { "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias", "Add public alias to other bank account.", - """Creates the public alias for the other account OTHER_ACCOUNT_ID. + s"""Creates the public alias for the other account OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public. + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public. | - |Note: Public aliases are automatically generated for new 'other accounts', so this call should only be used if + |Note: Public aliases are automatically generated for new 'other accounts / counterparties', so this call should only be used if |the public alias was deleted. | |The VIEW_ID parameter should be a view the caller is permitted to access to and that has permission to create public aliases.""", @@ -1009,9 +1012,10 @@ trait APIMethods121 { "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias", "Update public alias of other bank account.", - """Updates the public alias of the other account OTHER_ACCOUNT_ID. + s"""Updates the public alias of the other account / counterparty OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public.""", + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1045,9 +1049,10 @@ trait APIMethods121 { "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias", "Delete Counterparty Public Alias", - """Deletes the public alias of the other account OTHER_ACCOUNT_ID. + s"""Deletes the public alias of the other account OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public.""", + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -1078,9 +1083,10 @@ trait APIMethods121 { "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", "Get Counterparty Private Alias", - """Returns the private alias of the other account OTHER_ACCOUNT_ID. + s"""Returns the private alias of the other account OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public.""", + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -1113,9 +1119,10 @@ trait APIMethods121 { "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", "Create Counterparty Private Alias", - """Creates a private alias for the other account OTHER_ACCOUNT_ID. + s"""Creates a private alias for the other account OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public.""", + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1150,9 +1157,10 @@ trait APIMethods121 { "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", "Update Counterparty Private Alias", - """Updates the private alias of the counterparty (AKA other account) OTHER_ACCOUNT_ID. + s"""Updates the private alias of the counterparty (AKA other account) OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public.""", + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", Extraction.decompose(AliasJSON("An Alias")), emptyObjectJson, emptyObjectJson :: Nil, @@ -1187,9 +1195,10 @@ trait APIMethods121 { "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias", "Delete Counterparty Private Alias", - """Deletes the private alias of the other account OTHER_ACCOUNT_ID. + s"""Deletes the private alias of the other account OTHER_ACCOUNT_ID. | - |OAuth authentication is required if the view is not public.""", + |${authenticationRequiredMessage(false)} + |Authentication is required if the view is not public.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -2104,7 +2113,9 @@ trait APIMethods121 { "Add comment.", """Posts a comment about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | - |OAuth authentication is required since the comment is linked with the user.""", + |${authenticationRequiredMessage(false)} + | + |Authentication is required since the comment is linked with the user.""", Extraction.decompose(PostTransactionCommentJSON("Why did we spend money on this again?")), emptyObjectJson, emptyObjectJson :: Nil, @@ -2202,9 +2213,11 @@ Authentication via OAuth is required if the view is not public.""", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags", "Add a tag.", - """Posts a tag about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. + s"""Posts a tag about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | - |OAuth authentication is required since the tag is linked with the user.""", + |${authenticationRequiredMessage(true)} + | + |Authentication is required as the tag is linked with the user.""", Extraction.decompose(PostTransactionTagJSON("holiday")), emptyObjectJson, emptyObjectJson :: Nil, @@ -2304,9 +2317,11 @@ Authentication via OAuth is required if the view is not public.""", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images", "Add an image.", - """Posts an image about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. + s"""Posts an image about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | - |OAuth authentication is required since the image is linked with the user.""", + |${authenticationRequiredMessage(true)} + | + |The image is linked with the user.""", Extraction.decompose(PostTransactionImageJSON("The new printer", "www.example.com/images/printer.png")), emptyObjectJson, emptyObjectJson :: Nil, @@ -2405,9 +2420,11 @@ Authentication via OAuth is required if the view is not public.""", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", "Add where tag.", - """Creates a "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). + s"""Creates a "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). | - |OAuth authentication is required since the geo tag is linked with the user.""", + |${authenticationRequiredMessage(true)} + | + |The geo tag is linked with the user.""", Extraction.decompose(PostTransactionWhereJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, emptyObjectJson :: Nil, @@ -2442,9 +2459,11 @@ Authentication via OAuth is required if the view is not public.""", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", "Update where tag.", - """Updates the "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). + s"""Updates the "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). | - |OAuth authentication is required since the geo tag is linked with the user.""", + |${authenticationRequiredMessage(true)} + | + |The geo tag is linked with the user.""", Extraction.decompose(PostTransactionWhereJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))), emptyObjectJson, emptyObjectJson :: Nil, @@ -2479,9 +2498,11 @@ Authentication via OAuth is required if the view is not public.""", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", "Delete where tag.", - """Deletes the where tag of the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). - -Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the geo tag.""", + s"""Deletes the where tag of the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). + | + |${authenticationRequiredMessage(true)} + | + |The user must either have owner privileges for this account, or must be the user that posted the geo tag.""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index da84316a2..4305087c3 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -564,12 +564,12 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ "POST", "/banks/BANK_ID/customer", "Add a customer.", - """Add a customer linked to the currently authenticated user. + s"""Add a customer linked to the currently authenticated user. |The Customer resource stores the customer number, legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc. |This call may require additional permissions/role in the future. |For now the authenticated user can create at most one linked customer. |Dates need to be in the format 2013-01-21T23:08:00Z - |OAuth authentication is required. + |${authenticationRequiredMessage(true)} |Note: This call is depreciated in favour of v.2.0.0 createCustomer |""", Extraction.decompose(PostCustomerJson("687687678", "Joe David Bloggs", diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 0e9e32c96..e4344fc7a 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -131,13 +131,15 @@ trait APIMethods200 { "GET", "/accounts", "Get all Accounts at all Banks.", - """Get all accounts at all banks the User has access to (Authenticated + Anonymous access). + s"""Get all accounts at all banks the User has access to (Authenticated + Anonymous access). |Returns the list of accounts at that the user has access to at all banks. |For each account the API returns the account ID and the available views. | |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views. If |the user is authenticated, the list will contain non-public accounts to which the user has access, in addition to |all public accounts. + | + |${authenticationRequiredMessage(false)} |""", emptyObjectJson, emptyObjectJson, @@ -163,11 +165,12 @@ trait APIMethods200 { "GET", "/my/accounts", "Get Accounts at all Banks (Private)", - """Get private accounts at all banks (Authenticated access) + s"""Get private accounts at all banks (Authenticated access) |Returns the list of accounts containing private views for the user at all banks. |For each account the API returns the ID and the available views. | - |Authentication via OAuth is required.""", + |${authenticationRequiredMessage(true)} + |""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -207,9 +210,12 @@ trait APIMethods200 { "GET", "/accounts/public", "Get Public Accounts at all Banks.", - """Get public accounts at all banks (Anonymous access). + s"""Get public accounts at all banks (Anonymous access). |Returns the list of accounts containing public views at all banks - |For each account the API returns the ID and the available views. Authentication via OAuth is required.""", + |For each account the API returns the ID and the available views. + | + |${authenticationRequiredMessage(true)} + |""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -242,11 +248,11 @@ trait APIMethods200 { "GET", "/banks/BANK_ID/accounts", "Get Accounts at one Bank (Public and Private).", - """Get accounts at one bank that the user has access to (Authenticated + Anonymous access). + s"""Get accounts at one bank that the user has access to (Authenticated + Anonymous access). |Returns the list of accounts at BANK_ID that the user has access to. |For each account the API returns the account ID and the available views. | - |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views. + |If the user is not authenticated, the list will contain only the accounts providing public views. """, emptyObjectJson, emptyObjectJson, @@ -370,10 +376,10 @@ trait APIMethods200 { "GET", "/customers/CUSTOMER_ID/kyc_documents", "Get KYC Documents for Customer", - """Get KYC (know your customer) documents for a customer + s"""Get KYC (know your customer) documents for a customer |Get a list of documents that affirm the identity of the customer |Passport, driving licence etc. - |Authentication is required.""", + |${authenticationRequiredMessage(false)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -405,9 +411,9 @@ trait APIMethods200 { "GET", "/customers/CUSTOMER_ID/kyc_media", "Get KYC Media for a customer", - """Get KYC media (scans, pictures, videos) that affirms the identity of the customer. + s"""Get KYC media (scans, pictures, videos) that affirms the identity of the customer. | - |Authentication is required.""", + |${authenticationRequiredMessage(true)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -438,10 +444,10 @@ trait APIMethods200 { "GET", "/customers/CUSTOMER_ID/kyc_checks", "Get KYC Checks for current Customer", - """Get KYC checks for the logged in customer + s"""Get KYC checks for the logged in customer |Messages sent to the currently authenticated user. | - |Authentication via OAuth is required.""", + |${authenticationRequiredMessage(true)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -471,9 +477,9 @@ trait APIMethods200 { "GET", "/customers/CUSTOMER_ID/kyc_statuses", "Get the KYC statuses for a customer", - """Get the KYC statuses for a customer over time + s"""Get the KYC statuses for a customer over time | - |Authentication is required.""", + |${authenticationRequiredMessage(true)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -504,9 +510,9 @@ trait APIMethods200 { "GET", "/banks/BANK_ID/customers/CUSTOMER_ID/social_media_handles", "Get social media handles for a customer", - """Get social media handles for a customer. + s"""Get social media handles for a customer. | - |Authentication via OAuth is required.""", + |${authenticationRequiredMessage(true)}""", emptyObjectJson, emptyObjectJson, emptyObjectJson :: Nil, @@ -542,7 +548,11 @@ trait APIMethods200 { "POST", "/banks/BANK_ID/customers/CUSTOMER_NUMBER/kyc_documents", "Add KYC Document.", - "Add a KYC document for the customer specified by CUSTOMER_NUMBER. KYC Documents contain the document type (e.g. passport), place of issue, expiry etc. ", + s"""Add a KYC document for the customer specified by CUSTOMER_NUMBER. KYC Documents contain the document type (e.g. passport), + | place of issue, expiry etc. + |${authenticationRequiredMessage(true)} + | + |""", Extraction.decompose(KycDocumentJSON("wuwjfuha234678", "1234", "passport", "123567", exampleDate, "London", exampleDate)), emptyObjectJson, emptyObjectJson :: Nil, @@ -1145,7 +1155,7 @@ trait APIMethods200 { |The payee is set in the request body. Money goes into the BANK_ID and ACCOUNT_IDO specified in the request body. | | - | + |${authenticationRequiredMessage(true)} | |""", Extraction.decompose(TransactionRequestBodyJSON ( @@ -1528,6 +1538,8 @@ trait APIMethods200 { // + + resourceDocs += ResourceDoc( createCustomer, apiVersion, @@ -1535,12 +1547,12 @@ trait APIMethods200 { "POST", "/banks/BANK_ID/customers", "Create Customer.", - """Add a customer linked to the user specified by user_id + s"""Add a customer linked to the user specified by user_id |The Customer resource stores the customer number, legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc. |This call may require additional permissions/role in the future. |For now the authenticated user can create at most one linked customer. |Dates need to be in the format 2013-01-21T23:08:00Z - |OAuth authentication is required. + |${authenticationRequiredMessage(true)} |""", Extraction.decompose(CreateCustomerJson("user_id to attach this customer to e.g. 123213", "new customer number 687687678", "Joe David Bloggs", "+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), @@ -1682,10 +1694,10 @@ trait APIMethods200 { "POST", "/banks/user_customer_links", "Create user customer link.", - """Link a customer and an user + s"""Link a customer and an user |This call may require additional permissions/role in the future. |For now the authenticated user can create at most one linked customer. - |OAuth authentication is required. + |${authenticationRequiredMessage(true)} |""", Extraction.decompose(CreateUserCustomerLinkJSON("be106783-b4fa-48e6-b102-b178a11a8e9b", "02141bc6-0a69-4fba-b4db-a17e5fbbbdcc")), emptyObjectJson, From e2ae5c390aa0b66b1f197e998ab9fb33e7dbee47 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 15 Jul 2016 02:13:45 +0100 Subject: [PATCH 621/702] fixing merge --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 1e7fefde9..7a6cdb17f 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -648,7 +648,6 @@ trait APIMethods200 { lazy val addKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "customers" :: customerId :: "kyc_check" :: Nil JsonPost json -> _ => { - case "banks" :: BankId(bankId) :: "customers" :: customerNumber :: "kyc_check" :: Nil JsonPost json -> _ => { // customerNumber is in url and duplicated in postedData. remove from that? user => { for { From 2718b37d4877a93c643b2d877def49576ab09729 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 15 Jul 2016 12:52:30 +0200 Subject: [PATCH 622/702] This closes #94 - v2.0.0 createUser returns 400 instead of 409 if there is a duplicate username --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- src/test/scala/code/api/v2_0_0/CreateUserTest.scala | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 7a6cdb17f..105d6a38e 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1365,7 +1365,7 @@ trait APIMethods200 { } } else { - Full(errorJsonResponse("User with the same email already exists.")) + Full(errorJsonResponse("User with the same email already exists.", 409)) } } } diff --git a/src/test/scala/code/api/v2_0_0/CreateUserTest.scala b/src/test/scala/code/api/v2_0_0/CreateUserTest.scala index 39526763c..633737c61 100644 --- a/src/test/scala/code/api/v2_0_0/CreateUserTest.scala +++ b/src/test/scala/code/api/v2_0_0/CreateUserTest.scala @@ -111,5 +111,18 @@ class CreateUserTest extends V200ServerSetup with BeforeAndAfter { requestToken.value.size should not equal (0) } + scenario("we try to create a same user again", CreateUser) { + When("we create a same user") + val params = Map("email" -> EMAIL, + "password" -> PASSWORD, + "first_name" -> FIRSTNAME, + "last_name" -> LASTNAME) + + val request = (v2_0Request / "users").POST + val response = makePostRequest(request, write(params)) + Then("we should get a 409 created code") + response.code should equal(409) + } + } } \ No newline at end of file From 17e02b5a273ab492d749b4e94d7a504c6b713610 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 15 Jul 2016 14:47:26 +0200 Subject: [PATCH 623/702] v2.0.0 createCustomer doesn't return customer #95 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 105d6a38e..72ed3ff0b 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -10,6 +10,7 @@ import code.api.util.ApiRole._ import code.api.util.{ApiRole, ErrorMessages} import code.api.v1_2_1.OBPAPI1_2_1._ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121, JSONFactory => JSONFactory121} +import code.api.v1_4_0.JSONFactory1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, CustomerFaceImageJson, TransactionRequestAccountJSON} import code.entitlement.Entitlement import code.search.{elasticsearchMetrics, elasticsearchWarehouse} @@ -1599,7 +1600,8 @@ trait APIMethods200 { userCustomerLink <- booleanToBox(UserCustomerLink.userCustomerLink.vend.getUserCustomerLink(user_id, customer.customerId).isEmpty == true) ?~ ErrorMessages.CustomerAlreadyExistsForUser userCustomerLink <- UserCustomerLink.userCustomerLink.vend.createUserCustomerLink(user_id, customer.customerId, exampleDate, true) ?~! "Could not create user_customer_links" } yield { - val successJson = Extraction.decompose(customer) + val json = JSONFactory1_4_0.createCustomerJson(customer) + val successJson = Extraction.decompose(json) successJsonResponse(successJson, 201) } } From 70683878237a8686bba0d93e31fc2b46704c0894 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 15 Jul 2016 18:33:15 +0200 Subject: [PATCH 624/702] Code cleanup --- pom.xml | 8 +++--- project/build.scala | 2 +- .../bankconnectors/KafkaMappedConnector.scala | 6 ++--- .../scala/code/model/dataAccess/OBPUser.scala | 26 +++++++++++-------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index f0074d340..8f49b5c14 100644 --- a/pom.xml +++ b/pom.xml @@ -286,12 +286,12 @@ org.apache.maven.plugins maven-war-plugin - 2.3 + 2.6 org.apache.maven.plugins maven-resources-plugin - 2.5 + 3.0.1 default-copy-resources @@ -367,7 +367,7 @@
    - + diff --git a/project/build.scala b/project/build.scala index 910a377ab..a4679de04 100755 --- a/project/build.scala +++ b/project/build.scala @@ -77,7 +77,7 @@ object LiftProjectBuild extends Build { name := pom.name, resolvers ++= Seq( "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases", - "Java.net Maven2 Repository" at "http://download.java.net/maven/2/", + "Java.net Maven3 Repository" at "http://download.java.net/maven/3/", "Scala-Tools Dependencies Repository for Releases" at "http://scala-tools.org/repo-releases", "Scala-Tools Dependencies Repository for Snapshots" at "http://scala-tools.org/repo-snapshots"), diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 06c717ca0..f827288c4 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -49,10 +49,10 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable // Generate random uuid to be used as request-response match id reqId <- tryo {UUID.randomUUID().toString} u <- tryo{cachedUser.getOrElseUpdate( argList.toString, () => process(reqId, "getUser", argList).extract[KafkaInboundValidatedUser])} - recEmail <- tryo{u.email} ?~ {ErrorMessages.UserNotFoundByEmail} - user <- tryo{new KafkaInboundUser( recEmail, password, recEmail)} + recEmail <- tryo{u.email} } yield { - user + if (username == u.email) new KafkaInboundUser( recEmail, password, recEmail) + else null } } diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 532db5d3b..2c03395f0 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -386,19 +386,23 @@ import net.liftweb.util.Helpers._ password_ <- S.param("password") user_ <- getUserFromKafka(username_, password_) } yield { - logUserIn(user_, () => { - S.notice(S.?("logged.in")) - preLoginState() - S.redirectTo(homePage) - }) - for { - u <- APIUser.find(By(APIUser.email, user_.email)) - v <- tryo{KafkaMappedConnector.updateUserAccountViews(u)} + if (user != null) { + logUserIn(user_, () => { + S.notice(S.?("logged.in")) + preLoginState() + S.redirectTo(homePage) + }) + for { + u <- APIUser.find(By(APIUser.email, user_.email)) + v <- tryo { + KafkaMappedConnector.updateUserAccountViews(u) + } + } + user_ } - user_ + else + userLoginFailed } - if (extUser.isEmpty) - userLoginFailed } } } From 46d024fc91bbf59d9be579dfab5f6d916f41312b Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 15 Jul 2016 21:50:17 +0200 Subject: [PATCH 625/702] Added containerPort to build.scala for running with sbt --- project/build.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/project/build.scala b/project/build.scala index a4679de04..1e6eab21b 100755 --- a/project/build.scala +++ b/project/build.scala @@ -1,17 +1,19 @@ import sbt._ import Keys._ import com.earldouglas.xwp._ +import com.earldouglas.xwp.WebappPlugin +import com.earldouglas.xwp.ContainerPlugin.autoImport._ object LiftProjectBuild extends Build { override lazy val settings = super.settings ++ buildSettings lazy val buildSettings = Seq( - organization := pom.groupId, - version := pom.version + organization := pom.groupId, + version := pom.version ) - lazy val opanBank = Project( + lazy val openBank = Project( pom.artifactId, base = file("."), settings = defaultSettings ++ pom.settings) @@ -68,7 +70,8 @@ object LiftProjectBuild extends Build { lazy val settings = Seq( scalaVersion := pomScalaVersion, libraryDependencies ++= pomDeps, - resolvers ++= pomRepos + resolvers ++= pomRepos, + containerPort := 8080 ) } From 7aabe28b8a57469786cf5229f7e541d201bfec7a Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 18 Jul 2016 10:51:16 +0200 Subject: [PATCH 626/702] Added scala-compiler and scala-library versions to pom.xml --- pom.xml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 8f49b5c14..5a7171d70 100644 --- a/pom.xml +++ b/pom.xml @@ -161,11 +161,6 @@ amqp_2.6_${scala.version} 1.4-SNAPSHOT - - oauth.signpost - signpost-commonshttp4 - 1.2.1.2 - org.pegdown pegdown @@ -191,6 +186,23 @@ elastic4s-core_2.11 2.3.0 + + + org.scala-lang + scala-compiler + ${scala.compiler} + compile + + + org.scala-lang + scala-library + ${scala.compiler} + + + oauth.signpost + signpost-commonshttp4 + 1.2.1.2 + From 99ba068013746ddabf0fa26e4a3e9e90d0e5cf7c Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 18 Jul 2016 11:09:10 +0200 Subject: [PATCH 627/702] Trying shorter max-classfile-name for docker build --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a7171d70..9570c3d2c 100644 --- a/pom.xml +++ b/pom.xml @@ -282,7 +282,7 @@ -dependencyfile ${project.build.directory}/.scala_dependencies -Xmax-classfile-name - 180 + 78
    From ae0403161fe36d80d92a4cec603cb536773882b8 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 18 Jul 2016 11:23:09 +0200 Subject: [PATCH 628/702] Trying shorter max-classfile-name for docker build. 2nd try --- project/build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.scala b/project/build.scala index 1e6eab21b..0082a8394 100755 --- a/project/build.scala +++ b/project/build.scala @@ -85,7 +85,7 @@ object LiftProjectBuild extends Build { "Scala-Tools Dependencies Repository for Snapshots" at "http://scala-tools.org/repo-snapshots"), // compile options - scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked"), + scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-Xmax-classfile-name", "78") , javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation"), // show full stack traces From 767a19e63b636d373caacd29b9b5af1a00b3a915 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 18 Jul 2016 11:42:44 +0200 Subject: [PATCH 629/702] Added sections Get Started and SDKs Showcases to landing page --- src/main/webapp/index.html | 115 +++++++++++++++++++++----- src/main/webapp/media/css/website.css | 94 ++++++++++++++++++++- 2 files changed, 183 insertions(+), 26 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 11b151985..132817698 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -36,35 +36,56 @@ Berlin 13359, Germany + -
    -

    Get Started

    -

    -

      -
    1. Explore the API here using the example customer logins here.
    2. -
    3. Get API keys and SDKs.
    4. -
    -

    - -
    - -

    Questions?

    - -

    -

    -

    - -
    +
    +

    Get Started

    +
    +
    + +
    +
    + +
    +
    +

    Create an account

    +

    First, create a free developer account on this sandbox and request a developer key. You will be asked to submit basic information about your app at this stage. Register here.

    +
    +
    +
    +
    +

    Connect your app

    +

    Use our SDKs to connect your app to the Open Bank Project APIs. You will need your developer key, which was provided to you when you created an account. See all available APIs on the API Explorer. Please make sure you are using the correct base URL.

    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +

    Test your app using customer data

    +

    Once your app is connected, you can test it using test customer credentials. You can find a list of available customer logins for the sandbox here.

    +
    +
    + +

    Your APIs

    @@ -160,6 +181,56 @@ Berlin 13359, Germany
    +
    +

    SDK Showcases

    +
    + +

    Python

    + By OpenBankProject +
    +
    + +

    Django

    + By Azd325 +
    +
    + +

    NodeJS

    + By OpenBankProject +
    +
    + +

    Mac

    + By T0rst +
    +
    + +

    IOS

    + By T0rst +
    +
    + +

    Android

    + By OpenBankProject +
    +
    + +

    Scala (liftweb)

    + By OpenBankProject +
    +
    + +

    PHP

    + By Solonas +
    +
    + +

    C#

    + By Sweechem
    (this has a nice OAuth walk through with C# snippets) +
    +
    + +
    diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 00e140fc3..b911da2bf 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -196,6 +196,11 @@ input.submit { min-height: 463px; } +#main-about h1 { + color: white; + font-size: 36px; +} + #main-about p.about-text { padding: 10px 180px 40px 180px; font-size: 20px; @@ -241,9 +246,63 @@ input.submit { } +#main-get_started { + text-align: left; + margin: 40px auto; + background-color: #f1f4f8; +} + +.main-get_started-row { + display: table-row; +} +.main-get_started-cell { + display: table-cell; + vertical-align: top; + color: #838b98; +} +.main-get_started-cell:nth-child(1) { + width: 45%; + padding: 0 10px; +} + +.main-get_started-cell:nth-child(2) { + width: 95px; + height: 300px; +} +.main-get_started-cell:nth-child(2) img { + margin: 0 10px; + display: block; +} + +.main-get_started-cell:nth-child(3) { + width: 45%; + padding: 0 10px; +} + +.main-get_started-icon { + text-align: center; +} + +#main-get_started h1 { + text-align: center; + font-weight: bold; + font-size: 35px; + margin: 30px 0 30px 0; + display: table-caption; +} + +#main-get_started h2 { + font-weight: bold; + font-size: 24px; + color: #333333; +} + +#main-get_api_key { + padding: 30px 0; +} + #main-apis { text-align: left; - /*margin: 60px 0 60px 0;*/ margin: 40px auto; display: table; } @@ -281,12 +340,12 @@ input.submit { margin-top: 13px; } -#main-apiexplorer { +#main-apiexplorer, #main-get_api_key { width: 300px; margin: 30px auto; - text-align:center; + text-align: center; } -#main-apiexplorer a { +#main-apiexplorer a, #main-get_api_key a { border: 3px solid green; border-radius: 25px; color: green; @@ -295,6 +354,33 @@ input.submit { } +#main-showcases { + margin: 50px auto; + padding: 60px 0; + color: white; + background-color: #31b9ce; +} + +#main-showcases h1 { + margin-bottom: 60px; + font-size: 36px; +} + +#main-showcases h2 { + font-size: 24px; +} + +.main-showcases-item { + display: inline-block; + margin: 30px 10px; + width: 230px; +} + +.main-showcases-item span { + font-size: small; +} + + #main-partners { margin: 50px auto 0 auto; text-align: center; From 1b5b24f1ad60e12df09d3fc19e45d00369889533 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 18 Jul 2016 12:00:39 +0200 Subject: [PATCH 630/702] Updates for new sections on landing page --- src/main/webapp/index.html | 93 ++++++++++++++------------- src/main/webapp/media/css/website.css | 4 ++ 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 132817698..836493ac6 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -183,51 +183,54 @@ Berlin 13359, Germany

    SDK Showcases

    -
    - -

    Python

    - By OpenBankProject -
    -
    - -

    Django

    - By Azd325 -
    -
    - -

    NodeJS

    - By OpenBankProject -
    -
    - -

    Mac

    - By T0rst -
    -
    - -

    IOS

    - By T0rst -
    -
    - -

    Android

    - By OpenBankProject -
    -
    - -

    Scala (liftweb)

    - By OpenBankProject -
    -
    - -

    PHP

    - By Solonas -
    -
    - -

    C#

    - By Sweechem
    (this has a nice OAuth walk through with C# snippets) -
    + +
    + +

    Python

    + By OpenBankProject +
    +
    + +

    Django

    + By Azd325 +
    +
    + +

    NodeJS

    + By OpenBankProject +
    +
    + +

    Mac

    + By T0rst +
    +
    + +

    IOS

    + By T0rst +
    +
    + +

    Android

    + By OpenBankProject +
    +
    + +

    Scala (liftweb)

    + By OpenBankProject +
    +
    + +

    PHP

    + By Solonas +
    +
    + +

    C#

    + By Sweechem
    (this has a nice OAuth walk through with C# snippets) +
    + +

    Please make sure you are using the correct sandbox domain in all calls when using the SDKs. In doubt, drop us a line.

    diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index b911da2bf..48f715ee5 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -370,6 +370,10 @@ input.submit { font-size: 24px; } +#main-showcases p { + padding: 0 10px; +} + .main-showcases-item { display: inline-block; margin: 30px 10px; From 8dbe61d73277ecd33a4b5992699f6e64d39915cb Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 18 Jul 2016 16:21:21 +0200 Subject: [PATCH 631/702] v2.0.0 createCustomer doesn't return customer #95 --- .../scala/code/api/v2_0_0/CustomerTest.scala | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/test/scala/code/api/v2_0_0/CustomerTest.scala diff --git a/src/test/scala/code/api/v2_0_0/CustomerTest.scala b/src/test/scala/code/api/v2_0_0/CustomerTest.scala new file mode 100644 index 000000000..e0578e821 --- /dev/null +++ b/src/test/scala/code/api/v2_0_0/CustomerTest.scala @@ -0,0 +1,77 @@ +package code.api.v2_0_0 + +import java.text.SimpleDateFormat +import code.api.DefaultUsers +import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, CustomerJson} +import code.customer.{MappedCustomer} +import code.model.{BankId} +import code.api.util.APIUtil.OAuth._ +import net.liftweb.json.Serialization.write + +class CustomerTest extends V200ServerSetup with DefaultUsers { + + val exampleDateString: String = "22/08/2013" + val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") + val exampleDate = simpleDateFormat.parse(exampleDateString) + + val mockBankId = BankId("testBank1") + val mockCustomerNumber = "9393490320" + + + override def beforeAll() { + super.beforeAll() + } + + override def afterAll() { + super.afterAll() + MappedCustomer.bulkDelete_!!() + } + + feature("Assuring that create customer, v2.0.0, feedback and get customer, v1.4.0, feedback are the same") { + + scenario("There is a user, and the bank in questions has customer info for that user - v2.0.0") { + Given("The bank in question has customer info") + val testBank = mockBankId + + val customerPostJSON = CreateCustomerJson( + user_id = obpuser1.userId, + customer_number = mockCustomerNumber, + legal_name = "Someone", + mobile_phone_number = "125245", + email = "hello@hullo.com", + face_image = CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), + date_of_birth = exampleDate, + relationship_status = "Single", + dependants = 1, + dob_of_dependants = List(exampleDate), + highest_education_attained = "Bachelor’s Degree", + employment_status = "Employed", + kyc_status = true, + last_ok_date = exampleDate + ) + + val requestPost = (v2_0Request / "banks" / testBank.value / "customers").POST <@ (user1) + val responsePost = makePostRequest(requestPost, write(customerPostJSON)) + + Then("We should get a 201") + responsePost.code should equal(201) + + And("We should get the right information back") + val infoPost = responsePost.body.extract[CustomerJson] + + When("We make the request") + val requestGet = (v1_4Request / "banks" / testBank.value / "customer").GET <@ (user1) + val responseGet = makeGetRequest(requestGet) + + Then("We should get a 200") + responseGet.code should equal(200) + + And("We should get the right information back") + val infoGet = responseGet.body.extract[CustomerJson] + + And("POST feedback and GET feedback must be the same") + infoGet should equal(infoPost) + } + } + +} From 717a7eeceb9112019db9a026c3e5bc7350e50e02 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 19 Jul 2016 09:30:42 +0200 Subject: [PATCH 632/702] Added sections FAQ, Support and Start building to landing page --- src/main/scala/code/snippet/WebUI.scala | 15 ++- src/main/webapp/index.html | 127 +++++++++++++++++++++++- src/main/webapp/media/css/website.css | 84 +++++++++++++++- 3 files changed, 221 insertions(+), 5 deletions(-) diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index d6da487ad..9c8d66913 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -69,6 +69,19 @@ class WebUI extends Loggable{ + // Link to API + def apiLink: CssSel = { + val hostname = scala.xml.Unparsed(Props.get("hostname", "")) + ".api-link a *" #> hostname & + ".api-link a [href]" #> hostname + } + + // Link to Sandbox credentials + def sandboxCredentialsLink: CssSel = { + // github removes dots when creating new sections with dots in name in wiki page + val wikiAnchor = scala.xml.Unparsed(Props.get("hostname", "").replace("http://", "").replace("https://", "").replace(".", "").replace(":", "")) + ".sandbox-credentials a [href]" #> ("https://github.com/OpenBankProject/OBP-API/wiki/Sandbox-credentials#" + wikiAnchor) + } @@ -150,4 +163,4 @@ class WebUI extends Loggable{ "img [alt]" #> s"${i.altText}" } } -} \ No newline at end of file +} diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 836493ac6..17dda95dd 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -38,9 +38,9 @@ Berlin 13359, Germany
    @@ -234,9 +234,130 @@ Berlin 13359, Germany
    + +
    +

    FAQs

    + +
    +
    + +
    +
    +

    What is the correct base URL for this sandbox?

    +

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

    +
    + +
    +
    +

    I got a 404 error, what am I doing wrong

    +
      +
    1. + Avoid using trailing slashes, else, you would get a 404 error. Example:
      + .../obp/v1.4.0 200 OK
      + .../obp/v1.4.0/ 404 Not Found
      +
    2. +
    3. Double check parameters are spelt correctly (including http vs https etc.)
    4. +
    5. Check your encoding (use UTF8)
    6. +
    +
    +
    + +
    +
    + +
    +
    +

    How should I login?

    +

    There are two ways to authenticate a user. OAuth and Direct Login. If you are using this sandbox for a hackathon, we recommend you use Direct Login to authenticate as it is easier than the OAuth workflow. You can find more information on the Direct Login here +

    +
    +
    + +
    +
    +

    How can I use OAuth?

    +

    + For a walkthrough example with sample code, please see here. Further technical details including getting a request token, redirecting the user, getting an access token and accessing protected resources are available here
    + Please note we are using OAuth 1 +

    +
    +
    + +
    +
    + +
    +
    +

    Where can I read the API documentation?

    +

    + For the current stable API version see 1.4.0.
    + For the latest version (recommended) see 2.0.0 +

    +
    +
    + +
    +
    +

    What are some customer logins I can use?

    +

    During the login, the user of your app will be asked for a customer username/password. You can find example login to test your Direct Login or OAuth flow here.

    +
    +
    + +
    +
    + +
    +
    +

    How much does it cost?

    +

    It is free to use our sandbox for testing. Using live data may be subject to charges depending on the bank. Contact us to learn more about pricing options.

    +
    +
    + +
    +
    +

    Who owns the IP of the code I create?

    +

    Of course, you do!

    +
    +
    + +
    + + +
    +

    Get started building your application using this sandbox now, register for a developer key

    + +
    + + + + + + +
    +
    + +

    Email us

    + contact@openbankproject.com +
    +
    + +

    Twitter

    + @OpenBankProject +
    + + + +
    diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 48f715ee5..342cdf650 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -385,8 +385,62 @@ input.submit { } +#main-faq { + text-align: left; + margin: 40px auto; + padding:0 40px; + background-color: #f1f4f8; + border-collapse: separate; + border-spacing: 20px; +} + +.main-faq-row { + display: table-row; +} +.main-faq-cell { + display: table-cell; + vertical-align: top; + color: #838b98; +} +.main-faq-cell:nth-child(odd) { + width: 55px; +} +.main-faq-cell ol { + list-style-type: decimal; +} + +#main-faq h1 { + text-align: center; + font-weight: bold; + font-size: 36px; + margin: 30px 0 30px 0; + display: table-caption; +} + +#main-faq h2 { + font-size: 24px; + font-weight: bold; + color: #333333; +} + +#main-faq img { +} + + + +#main-start_building { + margin: 40px 0; + padding: 40px 80px; +} +#main-start_building h1 { + font-size: 36px; + font-weight: bold; + line-height: 40px; +} + + #main-partners { - margin: 50px auto 0 auto; + margin: 40px auto 0 auto; text-align: center; } @@ -402,6 +456,34 @@ input.submit { margin: 30px 30px 30px 30px; } + + +#main-support { + margin: 0; + margin-bottom: -20px; /* touch footer */ + padding: 60px; + color: white; + background-color: #31b9ce; +} + +#main-support h2 { + font-size: 24px; +} + +.main-support-item { + display: inline-block; + margin: 30px 10px; + width: 280px; +} +.main-support-item a { + color: white; +} + + + + + + .stretch { width: 100%; display: inline-block; From 53e3a4a720713f1c263aa98d7173b7546086f07b Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 19 Jul 2016 09:52:26 +0200 Subject: [PATCH 633/702] Updates for new sections on landing page --- src/main/webapp/index.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 17dda95dd..56aee4e22 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -78,7 +78,7 @@ Berlin 13359, Germany

    Test your app using customer data

    -

    Once your app is connected, you can test it using test customer credentials. You can find a list of available customer logins for the sandbox here.

    +

    Once your app is connected, you can test it using test customer credentials. You can find a list of available customer logins for this sandbox here.

    @@ -244,7 +244,11 @@ Berlin 13359, Germany

    What is the correct base URL for this sandbox?

    -

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

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

    From c514047a91a35e5251db72e87aad59109c5245f4 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 19 Jul 2016 10:41:38 +0200 Subject: [PATCH 634/702] Improved compliance with HTML standard for landing page --- src/main/scala/code/snippet/Login.scala | 4 +- src/main/scala/code/snippet/WebUI.scala | 7 ++- src/main/webapp/index.html | 78 ++++++++++++------------- src/main/webapp/media/css/website.css | 6 +- 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/main/scala/code/snippet/Login.scala b/src/main/scala/code/snippet/Login.scala index b2b30ac27..637382fed 100644 --- a/src/main/scala/code/snippet/Login.scala +++ b/src/main/scala/code/snippet/Login.scala @@ -58,7 +58,7 @@ class Login { if(OBPUser.loggedIn_?){ "*" #> NodeSeq.Empty } else { - ".login [action]" #> OBPUser.loginPageURL & + ".login [href]" #> OBPUser.loginPageURL & ".forgot [href]" #> { val href = for { menu <- OBPUser.lostPasswordMenuLoc @@ -100,4 +100,4 @@ class Login { // End of class -} \ No newline at end of file +} diff --git a/src/main/scala/code/snippet/WebUI.scala b/src/main/scala/code/snippet/WebUI.scala index 9c8d66913..b7cd7fed4 100644 --- a/src/main/scala/code/snippet/WebUI.scala +++ b/src/main/scala/code/snippet/WebUI.scala @@ -114,7 +114,12 @@ class WebUI extends Loggable{ } def overrideStyleSheet: CssSel = { - "#override_style_sheet [href]" #> scala.xml.Unparsed(Props.get("webui_override_style_sheet", "")) + val stylesheet = Props.get("webui_override_style_sheet", "") + if (stylesheet.isEmpty) { + "#override_style_sheet" #> "" + } else { + "#override_style_sheet [href]" #> scala.xml.Unparsed(stylesheet) + } } // Used to represent partners or sponsors of this API instance diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 56aee4e22..190adec6f 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -47,10 +47,10 @@ Berlin 13359, Germany

    Get Started

    - + create account
    - + item-1

    Create an account

    @@ -63,18 +63,18 @@ Berlin 13359, Germany

    Use our SDKs to connect your app to the Open Bank Project APIs. You will need your developer key, which was provided to you when you created an account. See all available APIs on the API Explorer. Please make sure you are using the correct base URL.

    - + item-2
    - + connect app
    - + test data
    - + item-3

    Test your app using customer data

    @@ -82,7 +82,7 @@ Berlin 13359, Germany
    - @@ -91,14 +91,14 @@ Berlin 13359, Germany

    Your APIs

    - + accounts

    Accounts

    Access the user's list of accounts and account information such as the balance. Explore...

    - + branches

    Branches, ATMs

    @@ -108,14 +108,14 @@ Berlin 13359, Germany
    - + transactions

    Transactions

    Access the transaction history and metadata of accounts. Explore...

    - + metadata

    Metadata

    @@ -125,14 +125,14 @@ Berlin 13359, Germany
    - + counterparties

    Counterparties

    -

    Access the payers & payees of an account including metadata such as their aliases, labels, logos and home pages. Explore...

    +

    Access the payers & payees of an account including metadata such as their aliases, labels, logos and home pages. Explore...

    - + entitlements

    Entitlements

    @@ -142,14 +142,14 @@ Berlin 13359, Germany
    - + messages

    Customer meetings, messages and video conferencing.

    Enable customer meetings, messages and video conferencing for KYC and CRM operations (uses third party video streaming). Explore...

    - + security

    Security challenges

    @@ -158,7 +158,7 @@ Berlin 13359, Germany
    - + requests

    Payments & Transaction requests

    @@ -166,7 +166,7 @@ Berlin 13359, Germany
    - + kyc

    Onboarding & KYC

    @@ -185,47 +185,47 @@ Berlin 13359, Germany

    SDK Showcases

    - + python

    Python

    By OpenBankProject
    - + django

    Django

    By Azd325
    - + nodejs

    NodeJS

    By OpenBankProject
    - + mac

    Mac

    By T0rst
    - + ios

    IOS

    By T0rst
    - + android

    Android

    By OpenBankProject
    - + scala

    Scala (liftweb)

    By OpenBankProject
    - + php

    PHP

    By Solonas
    - + csharp

    C#

    By Sweechem
    (this has a nice OAuth walk through with C# snippets)
    @@ -240,7 +240,7 @@ Berlin 13359, Germany
    - + plus

    What is the correct base URL for this sandbox?

    @@ -251,7 +251,7 @@ Berlin 13359, Germany

    - + plus

    I got a 404 error, what am I doing wrong

    @@ -269,7 +269,7 @@ Berlin 13359, Germany
    - + plus

    How should I login?

    @@ -277,7 +277,7 @@ Berlin 13359, Germany

    - + plus

    How can I use OAuth?

    @@ -290,7 +290,7 @@ Berlin 13359, Germany
    - + plus

    Where can I read the API documentation?

    @@ -300,7 +300,7 @@ Berlin 13359, Germany

    - + plus

    What are some customer logins I can use?

    @@ -310,14 +310,14 @@ Berlin 13359, Germany
    - + plus

    How much does it cost?

    It is free to use our sandbox for testing. Using live data may be subject to charges depending on the bank. Contact us to learn more about pricing options.

    - + plus

    Who owns the IP of the code I create?

    @@ -330,7 +330,7 @@ Berlin 13359, Germany

    Get started building your application using this sandbox now, register for a developer key

    - @@ -347,17 +347,17 @@ Berlin 13359, Germany
    - + twitter

    Twitter

    @OpenBankProject
    diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 342cdf650..6b53da2d2 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -297,7 +297,7 @@ input.submit { color: #333333; } -#main-get_api_key { +.main-get_api_key { padding: 30px 0; } @@ -340,12 +340,12 @@ input.submit { margin-top: 13px; } -#main-apiexplorer, #main-get_api_key { +#main-apiexplorer, .main-get_api_key { width: 300px; margin: 30px auto; text-align: center; } -#main-apiexplorer a, #main-get_api_key a { +#main-apiexplorer a, .main-get_api_key a { border: 3px solid green; border-radius: 25px; color: green; From 25fffd012a97902c45a1bebd4e951e207c9a30fe Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 19 Jul 2016 10:48:54 +0200 Subject: [PATCH 635/702] Set cell width of even cells to 410px in FAQ --- src/main/webapp/media/css/website.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 6b53da2d2..20779ddfb 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -405,6 +405,10 @@ input.submit { .main-faq-cell:nth-child(odd) { width: 55px; } +.main-faq-cell:nth-child(even) { + width: 410px; +} + .main-faq-cell ol { list-style-type: decimal; } From dfcc8b3c04fcf19524d1f9493680e7f7604bfbad Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 19 Jul 2016 13:01:21 +0100 Subject: [PATCH 636/702] Resource doc text and comment on v2.0.0 accountById --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 72ed3ff0b..22bdfdbaf 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -212,7 +212,7 @@ trait APIMethods200 { "/accounts/public", "Get Public Accounts at all Banks.", s"""Get public accounts at all banks (Anonymous access). - |Returns the list of accounts containing public views at all banks + |Returns accounts that contain at least one public view (a view where is_public is true) |For each account the API returns the ID and the available views. | |${authenticationRequiredMessage(true)} @@ -892,7 +892,7 @@ trait APIMethods200 { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "account" :: Nil JsonGet json => { user => for { - u <- user ?~! ErrorMessages.UserNotLoggedIn // Check we have a user (rather than error or empty) + u <- user ?~! ErrorMessages.UserNotLoggedIn // TODO. Allow anon for public views. (check previous version of this call) Check we have a user (rather than error or empty) bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // Check bank exists. account <- BankAccount(bank.bankId, accountId) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. availableViews <- Full(account.permittedViews(user)) @@ -1571,7 +1571,6 @@ trait APIMethods200 { canCreateCustomer <- tryo(hasEntitlement(bank.bankId.value, u.userId, CanCreateCustomer)) isLoggedUser <- booleanToBox(postedData.user_id.nonEmpty == false || canCreateCustomer == true || postedData.user_id.equalsIgnoreCase(u.userId)) ?~ "User can create a customer for themself only" checkAvailable <- tryo(assert(Customer.customerProvider.vend.checkCustomerNumberAvailable(bankId, postedData.customer_number) == true)) ?~! ErrorMessages.CustomerNumberAlreadyExists - // TODO The user id we expose should be a uuid . For now we have a long direct from the database. user_id <- tryo (if (postedData.user_id.nonEmpty) postedData.user_id else u.userId) ?~ s"Problem getting user_id" customer_user <- User.findByUserId(user_id) ?~! ErrorMessages.UserNotFoundById userCustomerLinks <- UserCustomerLink.userCustomerLink.vend.getUserCustomerLinks From 6b5c992f81284cb06f227411123f6c0303965db3 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 19 Jul 2016 15:43:00 +0200 Subject: [PATCH 637/702] Kafka upgrade to 0.10.0.0 --- pom.xml | 2 +- .../code/bankconnectors/KafkaHelper.scala | 27 ++----------------- .../bankconnectors/KafkaMappedConnector.scala | 8 +++--- 3 files changed, 6 insertions(+), 31 deletions(-) diff --git a/pom.xml b/pom.xml index 9570c3d2c..00e1a4794 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,7 @@ org.apache.kafka kafka_${scala.version} - 0.8.2.2 + 0.10.0.0 org.bouncycastle diff --git a/src/main/scala/code/bankconnectors/KafkaHelper.scala b/src/main/scala/code/bankconnectors/KafkaHelper.scala index 5b2f23d31..957b02c3b 100644 --- a/src/main/scala/code/bankconnectors/KafkaHelper.scala +++ b/src/main/scala/code/bankconnectors/KafkaHelper.scala @@ -28,33 +28,11 @@ import java.util.{Properties, UUID} import kafka.consumer.{Consumer, _} import kafka.message._ import kafka.producer.{KeyedMessage, Producer, ProducerConfig} -import kafka.utils.{Json, ZKStringSerializer, ZkUtils} +import kafka.utils.Json import net.liftweb.json import net.liftweb.json._ import net.liftweb.util.Props -import org.I0Itec.zkclient.ZkClient -object ZooKeeperUtils { - // gets brokers tracked by zookeeper - def getBrokers(zookeeper:String): List[String] = { - //connect to zookeeper client - val zkClient = new ZkClient(zookeeper, 30000, 30000, ZKStringSerializer) - // get list of all available kafka brokers - val brokers = for {broker <- ZkUtils.getAllBrokersInCluster(zkClient)} yield {broker.host +":"+ broker.port} - // close zookeeper client before returning the result - zkClient.close() - brokers.toList - } - // gets all topics tracked by zookeeper - def getTopics(zookeeper:String): List[String] = { - //connect to zookeeper client - val zkClient = new ZkClient(zookeeper, 30000, 30000, ZKStringSerializer) - // get list of all available kafka topics - val res = ZkUtils.getAllTopics(zkClient).toList - zkClient.close() - res - } -} class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").openOrThrowException("no kafka.zookeeper_host set"), val topic: String = Props.get("kafka.response_topic").openOrThrowException("no kafka.response_topic set"), @@ -118,11 +96,10 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").op } } -import code.bankconnectors.ZooKeeperUtils._ case class KafkaProducer( topic: String = Props.get("kafka.request_topic").openOrThrowException("no kafka.request_topic set"), - brokerList: String = getBrokers(Props.get("kafka.zookeeper_host")openOr("localhost:2181")).mkString(","), + brokerList: String = Props.get("kafka.host")openOr("localhost:9092").mkString(","), //getBrokers(Props.get("kafka.zookeeper_host")openOr("localhost:2181")).mkString(","), clientId: String = UUID.randomUUID().toString, synchronously: Boolean = true, compress: Boolean = true, diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index f827288c4..d03de5908 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -14,11 +14,9 @@ import code.metadata.wheretags.MappedWhereTag import code.model._ import code.model.dataAccess._ import code.sandbox.{CreateViewImpls, Saveable} -import code.tesobe.CashTransaction import code.transaction.MappedTransaction import code.transactionrequests.MappedTransactionRequest -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge} -import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge} +import code.transactionrequests.TransactionRequests.{TransactionRequest, TransactionRequestBody, TransactionRequestChallenge, TransactionRequestCharge } import code.util.{Helper, TTLCache} import code.views.Views import net.liftweb.common._ @@ -43,6 +41,8 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable val cachedPublicAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) val cachedUserAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL) + implicit val formats = net.liftweb.json.DefaultFormats + def getUser( username: String, password: String ): Box[KafkaInboundUser] = { for { argList <- tryo {Map[String, String]( "email" -> username.toLowerCase, "password" -> password )} @@ -101,8 +101,6 @@ object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable } } - implicit val formats = net.liftweb.json.DefaultFormats - def updateUserAccountViews( user: APIUser ) = { val accounts = for { email <- tryo {user.emailAddress} From 6a5f39a0aa3ff052e9152249215baff313b9914d Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Tue, 19 Jul 2016 16:14:27 +0200 Subject: [PATCH 638/702] Improved FAQ on landing page --- src/main/webapp/index.html | 146 +++++++++++--------------- src/main/webapp/media/css/website.css | 60 +++++++---- src/main/webapp/media/js/website.js | 15 ++- 3 files changed, 115 insertions(+), 106 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 190adec6f..8f7cc35bc 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -240,91 +240,73 @@ Berlin 13359, Germany
    - plus -
    -
    -

    What is the correct base URL for this sandbox?

    -

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

    -
    -
    - plus -
    -
    -

    I got a 404 error, what am I doing wrong

    -
      -
    1. - Avoid using trailing slashes, else, you would get a 404 error. Example:
      - .../obp/v1.4.0 200 OK
      - .../obp/v1.4.0/ 404 Not Found
      +
        +
      • +

        What is the correct base URL for this sandbox?

        +

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

      • -
      • Double check parameters are spelt correctly (including http vs https etc.)
      • -
      • Check your encoding (use UTF8)
      • -
    + +
  • +

    How should I login?

    +

    There are two ways to authenticate a user. OAuth and Direct Login. If you are using this sandbox for a hackathon, we recommend you use Direct Login to authenticate as it is easier than the OAuth workflow. You can find more information on the Direct Login here +

    +
  • + +
  • +

    Where can I read the API documentation?

    +

    + For the current stable API version see 1.4.0.
    + For the latest version (recommended) see 2.0.0 +

    + +
  • + +
  • +

    How much does it cost?

    +

    It is free to use our sandbox for testing. Using live data may be subject to charges depending on the bank. Contact us to learn more about pricing options.

    +
  • + +
    +
    +
      +
    • +

      I got a 404 error, what am I doing wrong

      +
        +
      1. + Avoid using trailing slashes, else, you would get a 404 error. Example:
        + .../obp/v1.4.0 200 OK
        + .../obp/v1.4.0/ 404 Not Found
        +
      2. +
      3. Double check parameters are spelt correctly (including http vs https etc.)
      4. +
      5. Check your encoding (use UTF8)
      6. +
      +
    • + +
    • +

      How can I use OAuth?

      +

      + For a walkthrough example with sample code, please see here. Further technical details including getting a request token, redirecting the user, getting an access token and accessing protected resources are available here
      + Please note we are using OAuth 1 +

      +
    • + +
    • +

      What are some customer logins I can use?

      +

      During the login, the user of your app will be asked for a customer username/password. You can find example login to test your Direct Login or OAuth flow here.

      +
    • + +
    • +

      Who owns the IP of the code I create?

      +

      Of course, you do!

      +
    • + +
    - -
    -
    - plus -
    -
    -

    How should I login?

    -

    There are two ways to authenticate a user. OAuth and Direct Login. If you are using this sandbox for a hackathon, we recommend you use Direct Login to authenticate as it is easier than the OAuth workflow. You can find more information on the Direct Login here -

    -
    -
    - plus -
    -
    -

    How can I use OAuth?

    -

    - For a walkthrough example with sample code, please see here. Further technical details including getting a request token, redirecting the user, getting an access token and accessing protected resources are available here
    - Please note we are using OAuth 1 -

    -
    -
    - -
    -
    - plus -
    -
    -

    Where can I read the API documentation?

    -

    - For the current stable API version see 1.4.0.
    - For the latest version (recommended) see 2.0.0 -

    -
    -
    - plus -
    -
    -

    What are some customer logins I can use?

    -

    During the login, the user of your app will be asked for a customer username/password. You can find example login to test your Direct Login or OAuth flow here.

    -
    -
    - -
    -
    - plus -
    -
    -

    How much does it cost?

    -

    It is free to use our sandbox for testing. Using live data may be subject to charges depending on the bank. Contact us to learn more about pricing options.

    -
    -
    - plus -
    -
    -

    Who owns the IP of the code I create?

    -

    Of course, you do!

    -
    -
    -
    diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 20779ddfb..9d64c21e2 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -390,27 +390,6 @@ input.submit { margin: 40px auto; padding:0 40px; background-color: #f1f4f8; - border-collapse: separate; - border-spacing: 20px; -} - -.main-faq-row { - display: table-row; -} -.main-faq-cell { - display: table-cell; - vertical-align: top; - color: #838b98; -} -.main-faq-cell:nth-child(odd) { - width: 55px; -} -.main-faq-cell:nth-child(even) { - width: 410px; -} - -.main-faq-cell ol { - list-style-type: decimal; } #main-faq h1 { @@ -422,13 +401,48 @@ input.submit { } #main-faq h2 { - font-size: 24px; + font-size: 18px; font-weight: bold; color: #333333; } -#main-faq img { +#main-faq .collapse { + background-repeat:no-repeat; + padding-left: 30px; + margin-bottom: 30px; + list-style: none; } +#main-faq .collapse.plus { + background-image: url('https://static.openbankproject.com/images/sandbox/faq/plus.png'); +} +#main-faq .collapse.minus { + background-image: url('https://static.openbankproject.com/images/sandbox/faq/minus.png'); +} +#main-faq .collapse:hover { + cursor: pointer; +} + +#main-faq ol { + list-style-type: decimal; + display: none; +} + +#main-faq p { + display: none; +} + +.main-faq-row { + display: table-row; +} + +.main-faq-cell { + display: table-cell; + vertical-align: top; + color: #838b98; + font-size: 16px; + width:485px; +} + diff --git a/src/main/webapp/media/js/website.js b/src/main/webapp/media/js/website.js index 72ed29aab..ed19d84bc 100644 --- a/src/main/webapp/media/js/website.js +++ b/src/main/webapp/media/js/website.js @@ -26,4 +26,17 @@ $(document).ready(function() { }); }); } -}); \ No newline at end of file + + + // FAQ shenanigans + $('#main-faq .collapse').click(function() { + var answer = $(this).find('h2').next(); + if ($(this).attr("class").indexOf("minus") >= 0) { + answer.hide(); + $(this).removeClass("minus").addClass("plus"); + } else { + answer.show(); + $(this).removeClass("plus").addClass("minus"); + } + }); +}); From 6461faefefde50c34a570bf3fcff640af28eabd6 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Tue, 19 Jul 2016 16:40:54 +0200 Subject: [PATCH 639/702] Fixed kafka brokerlist --- src/main/scala/code/bankconnectors/KafkaHelper.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/bankconnectors/KafkaHelper.scala b/src/main/scala/code/bankconnectors/KafkaHelper.scala index 957b02c3b..05a3aa896 100644 --- a/src/main/scala/code/bankconnectors/KafkaHelper.scala +++ b/src/main/scala/code/bankconnectors/KafkaHelper.scala @@ -52,7 +52,7 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").op props.put("auto.commit.interval.ms", "1000") props.put("zookeeper.session.timeout.ms", "6000") props.put("zookeeper.connection.timeout.ms", "6000") - props.put("consumer.timeout.ms", "40000") + props.put("consumer.timeout.ms", "20000") val config = new ConsumerConfig(props) config } @@ -99,7 +99,7 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host").op case class KafkaProducer( topic: String = Props.get("kafka.request_topic").openOrThrowException("no kafka.request_topic set"), - brokerList: String = Props.get("kafka.host")openOr("localhost:9092").mkString(","), //getBrokers(Props.get("kafka.zookeeper_host")openOr("localhost:2181")).mkString(","), + brokerList: String = Props.get("kafka.host")openOr("localhost:9092"), clientId: String = UUID.randomUUID().toString, synchronously: Boolean = true, compress: Boolean = true, From 22c76fe62bd15aae42763c1b07db00b0044fbac9 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 20 Jul 2016 09:08:05 +0200 Subject: [PATCH 640/702] Check 1.4.0 getCustomer uses UserCustomerLink #96 --- .../scala/code/api/v1_4_0/APIMethods140.scala | 10 +- .../code/customer/CustomerProvider.scala | 2 + .../customer/MappedCustomerProvider.scala | 11 +- .../MappedUserCustomerLink.scala | 5 + .../usercustomerlinks/UserCustomerLink.scala | 1 + .../scala/code/api/v1_4_0/CustomerTest.scala | 180 +++++++----------- 6 files changed, 93 insertions(+), 116 deletions(-) diff --git a/src/main/scala/code/api/v1_4_0/APIMethods140.scala b/src/main/scala/code/api/v1_4_0/APIMethods140.scala index 2ba79af64..52ff554f7 100644 --- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala +++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala @@ -90,7 +90,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ for { u <- user ?~! ErrorMessages.UserNotLoggedIn bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} - info <- Customer.customerProvider.vend.getCustomer(bankId, u) ?~ "No customer information found for current user" + ucls <- tryo{UserCustomerLink.userCustomerLink.vend.getUserCustomerLinkByUserId(u.userId)} ?~! ErrorMessages.CustomerDoNotExistsForUser + ucl <- tryo{ucls.find(x=>Customer.customerProvider.vend.getBankIdByCustomerId(x.customerId) == bankId.value)} + isEmpty <- booleanToBox(ucl.size > 0, ErrorMessages.CustomerDoNotExistsForUser) + u <- ucl + info <- Customer.customerProvider.vend.getCustomerByCustomerId(u.customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId } yield { val json = JSONFactory1_4_0.createCustomerJson(info) successJsonResponse(Extraction.decompose(json)) @@ -611,8 +615,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{ postedData.kyc_status, postedData.last_ok_date) ?~! "Could not create customer" } yield { - val successJson = Extraction.decompose(customer) - successJsonResponse(successJson) + val successJson = JSONFactory1_4_0.createCustomerJson(customer) + successJsonResponse(Extraction.decompose(successJson)) } } } diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala index cd3de10b6..b7a2ea051 100644 --- a/src/main/scala/code/customer/CustomerProvider.scala +++ b/src/main/scala/code/customer/CustomerProvider.scala @@ -19,6 +19,8 @@ trait CustomerProvider { def getCustomerByCustomerId(customerId: String): Box[Customer] + def getBankIdByCustomerId(customerId: String): Box[String] + def getCustomer(customerId: String, bankId : BankId): Box[Customer] def getUser(bankId : BankId, customerNumber : String) : Box[User] diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala index f93cae60f..e7a24d60d 100644 --- a/src/main/scala/code/customer/MappedCustomerProvider.scala +++ b/src/main/scala/code/customer/MappedCustomerProvider.scala @@ -37,6 +37,13 @@ object MappedCustomerProvider extends CustomerProvider { ) } + override def getBankIdByCustomerId(customerId: String): Box[String] = { + val customer: Box[MappedCustomer] = MappedCustomer.find( + By(MappedCustomer.mCustomerId, customerId) + ) + for (c <- customer) yield {c.mBank.get} + } + override def getCustomer(customerId: String, bankId : BankId): Box[Customer] = { MappedCustomer.find( By(MappedCustomer.mCustomerId, customerId), @@ -118,11 +125,11 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with } override def dateOfBirth: Date = mDateOfBirth.get override def relationshipStatus: String = mRelationshipStatus.get - override def dependents: Int = mDependents + override def dependents: Int = mDependents.get override def dobOfDependents: List[Date] = List(createdAt.get) override def highestEducationAttained: String = mHighestEducationAttained.get override def employmentStatus: String = mEmploymentStatus.get - override def kycStatus: Boolean = mKycStatus + override def kycStatus: Boolean = mKycStatus.get override def lastOkDate: Date = mLastOkDate.get } diff --git a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala index 010d6ffab..528c5d914 100644 --- a/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala +++ b/src/main/scala/code/usercustomerlinks/MappedUserCustomerLink.scala @@ -44,6 +44,11 @@ class MappedUserCustomerLink extends UserCustomerLink with LongKeyedMapper[Mappe By(MappedUserCustomerLink.mCustomerId, customerId)) } + override def getUserCustomerLinkByUserId(userId: String): List[UserCustomerLink] = { + MappedUserCustomerLink.findAll( + By(MappedUserCustomerLink.mUserId, userId)) + } + override def getUserCustomerLink(userId : String, customerId: String): Box[UserCustomerLink] = { MappedUserCustomerLink.find( By(MappedUserCustomerLink.mUserId, userId), diff --git a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala index d11b5056f..626551195 100644 --- a/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala +++ b/src/main/scala/code/usercustomerlinks/UserCustomerLink.scala @@ -23,6 +23,7 @@ trait UserCustomerLink { def createUserCustomerLink(userId: String, customerId: String, dateInserted: Date, isActive: Boolean): Box[UserCustomerLink] def getUserCustomerLink(customerId: String): Box[UserCustomerLink] + def getUserCustomerLinkByUserId(userId: String): List[UserCustomerLink] def getUserCustomerLink(userId: String, customerId: String): Box[UserCustomerLink] def getUserCustomerLinks: Box[List[UserCustomerLink]] } \ No newline at end of file diff --git a/src/test/scala/code/api/v1_4_0/CustomerTest.scala b/src/test/scala/code/api/v1_4_0/CustomerTest.scala index 969f5c989..fb81eef18 100644 --- a/src/test/scala/code/api/v1_4_0/CustomerTest.scala +++ b/src/test/scala/code/api/v1_4_0/CustomerTest.scala @@ -1,144 +1,102 @@ package code.api.v1_4_0 -import java.util.Date - +import java.text.SimpleDateFormat import code.api.DefaultUsers -import code.api.v1_4_0.JSONFactory1_4_0.CustomerJson -import code.customer.{CustomerFaceImage, Customer, CustomerProvider} -import code.model.{User, BankId} -import net.liftweb.common.{Full, Empty, Box} -import dispatch._ +import code.api.util.ErrorMessages +import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, CustomerJson} +import code.api.v2_0_0.{CreateUserCustomerLinkJSON, V200ServerSetup, CreateCustomerJson} +import code.customer.{MappedCustomer} +import code.model.{BankId} +import net.liftweb.json.JsonAST._ +import net.liftweb.json.Serialization._ import code.api.util.APIUtil.OAuth._ -class CustomerTest extends V140ServerSetup with DefaultUsers { +class CustomerTest extends V200ServerSetup with DefaultUsers { + + val exampleDateString: String = "22/08/2013" + val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") + val exampleDate = simpleDateFormat.parse(exampleDateString) val mockBankId = BankId("testBank1") + val mockCustomerNumber = "9393490320" - val mocCustomerId = "ddfc0cf8-82a0-42eb-9027-8cfb0e7988c9" - - case class MockFaceImage(date : Date, url : String) extends CustomerFaceImage - - case class MockCustomer(customerId: String, number: String, mobileNumber: String, - legalName: String, email: String, - faceImage: MockFaceImage, dateOfBirth: Date, - relationshipStatus: String, dependents: Int, - dobOfDependents: List[Date], highestEducationAttained: String, - employmentStatus: String, kycStatus: Boolean, lastOkDate: Date) extends Customer - - val format = new java.text.SimpleDateFormat("dd/MM/yyyy") - val mockCustomerFaceImage = MockFaceImage(new Date(1234000), "http://example.com/image1") - val mockCustomer = MockCustomer("uuid-aisuhuiuhuikjhfd", "123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(format.parse("30/03/2012"), format.parse("30/03/2012"), format.parse("30/03/2014")), "Bachelor’s Degree", "Employed", true, new Date(1234000)) - - object MockedCustomerProvider extends CustomerProvider { - - override def checkCustomerNumberAvailable(bankId : BankId, customerNumber : String) = {true} - - override def getCustomer(bankId: BankId, user: User): Box[Customer] = { - if(bankId == mockBankId) Full(mockCustomer) - else Empty - } - - override def getCustomerByCustomerId(customerId: String): Box[Customer] = { - if(customerId == mocCustomerId) Full(mockCustomer) - else Empty - } - - override def getCustomer(customerId: String, bankId: BankId): Box[Customer] = { - if(customerId == mocCustomerId && bankId == mockBankId) Full(mockCustomer) - else Empty - } - - override def getUser(bankId: BankId, customerId: String): Box[User] = Empty - override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage, - dateOfBirth: Date, - relationshipStatus: String, - dependents: Int, - dobOfDependents: List[Date], - highestEducationAttained: String, - employmentStatus: String, - kycStatus: Boolean, - lastOkDate: Date) : Box[Customer] = Empty - } override def beforeAll() { super.beforeAll() - //use the mock connector - Customer.customerProvider.default.set(MockedCustomerProvider) } override def afterAll() { super.afterAll() - //reset the default connector - Customer.customerProvider.default.set(Customer.buildOne) + MappedCustomer.bulkDelete_!!() } - feature("Getting a bank's customer info of the current user") { + feature("Assuring that create customer, v1.4.0, feedback and get customer, v1.4.0, feedback are the same") { - scenario("There is no current user") { - Given("There is no logged in user") - - When("We make the request") - val request = (v1_4Request / "banks" / mockBankId.value / "customer").GET - val response = makeGetRequest(request) - - Then("We should get a 400") - response.code should equal(400) - } - - scenario("There is a user, but the bank in questions has no customer info") { - Given("The bank in question has no customer info") - val testBank = BankId("test-bank") - val user = obpuser1 - - Customer.customerProvider.vend.getCustomer(testBank, user).isEmpty should equal(true) - - When("We make the request") - //TODO: need stronger link between obpuser1 and user1 - val request = (v1_4Request / "banks" / testBank.value / "customer").GET <@(user1) - val response = makeGetRequest(request) - - Then("We should get a 400") - response.code should equal(400) - } - - scenario("There is a user, and the bank in questions has customer info for that user") { + scenario("There is a user, and the bank in questions has customer info for that user - v1.4.0") { Given("The bank in question has customer info") val testBank = mockBankId - val user = obpuser1 - Customer.customerProvider.vend.getCustomer(testBank, user).isEmpty should equal(false) + val customerPostJSON = CreateCustomerJson( + user_id = obpuser1.userId, + customer_number = mockCustomerNumber, + legal_name = "Someone", + mobile_phone_number = "125245", + email = "hello@hullo.com", + face_image = CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate), + date_of_birth = exampleDate, + relationship_status = "Single", + dependants = 1, + dob_of_dependants = List(exampleDate), + highest_education_attained = "Bachelor’s Degree", + employment_status = "Employed", + kyc_status = true, + last_ok_date = exampleDate + ) + val requestPost = (v1_4Request / "banks" / testBank.value / "customer").POST <@ (user1) + val responsePost = makePostRequest(requestPost, write(customerPostJSON)) + Then("We should get a 200") + responsePost.code should equal(200) + And("We should get the right information back") + val infoPost = responsePost.body.extract[CustomerJson] + + When("We make the request without link user to customer") + val requestGetWithoutLink = (v1_4Request / "banks" / testBank.value / "customer").GET <@ (user1) + val responseGetWithoutLink = makeGetRequest(requestGetWithoutLink) + Then("We should get a 400") + responseGetWithoutLink.code should equal(400) + val error = for { JObject(o) <- responseGetWithoutLink.body; JField("error", JString(error)) <- o } yield error + And("We should get a message: " + ErrorMessages.CustomerDoNotExistsForUser) + error should contain (ErrorMessages.CustomerDoNotExistsForUser) + + + val customerId: String = (responsePost.body \ "customer_id") match { + case JString(i) => i + case _ => "" + } + + When("We link user to customer") + val uclJSON = CreateUserCustomerLinkJSON(user_id = obpuser1.userId, customer_id = customerId) + val requestPostUcl = (v2_0Request / "banks" / "user_customer_links").POST <@ (user1) + val responsePostUcl = makePostRequest(requestPostUcl, write(uclJSON)) + + Then("We should get a 201") + responsePostUcl.code should equal(201) + When("We make the request") - //TODO: need stronger link between obpuser1 and user1 - val request = (v1_4Request / "banks" / testBank.value / "customer").GET <@(user1) - val response = makeGetRequest(request) + val requestGet = (v1_4Request / "banks" / testBank.value / "customer").GET <@ (user1) + val responseGet = makeGetRequest(requestGet) Then("We should get a 200") - response.code should equal(200) + responseGet.code should equal(200) And("We should get the right information back") + val infoGet = responseGet.body.extract[CustomerJson] - val info = response.body.extract[CustomerJson] - val received = MockCustomer( - info.customer_id, - info.customer_number, - info.mobile_phone_number, - info.legal_name, - info.email, - MockFaceImage(info.face_image.date, info.face_image.url), - info.date_of_birth, - info.relationship_status, - info.dependants, - info.dob_of_dependants, - info.highest_education_attained, - info.employment_status, - info.kyc_status, - info.last_ok_date) - - received should equal(mockCustomer) + And("POST feedback and GET feedback must be the same") + infoGet should equal(infoPost) } - } From b6c4029aa126616c2391bd198882ac7dd9378633 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 20 Jul 2016 10:40:12 +0200 Subject: [PATCH 641/702] Add v2.0.0 getCustomers #101 --- src/main/scala/code/api/util/APIUtil.scala | 13 ++++++ .../code/api/v1_4_0/JSONFactory1_4_0.scala | 8 ++++ .../scala/code/api/v2_0_0/APIMethods200.scala | 44 ++++++++++++++++++- .../scala/code/api/v2_0_0/OBPAPI2_0_0.scala | 3 +- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 34741392d..62985f9a4 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -36,6 +36,7 @@ import code.api.Constant._ import code.api.DirectLogin import code.api.OAuthHandshake._ import code.api.v1_2.ErrorMessage +import code.customer.Customer import code.entitlement.Entitlement import code.metrics.APIMetrics import code.model._ @@ -626,4 +627,16 @@ Returns a string showed to the developer !Entitlement.entitlement.vend.getEntitlement(bankId, userId, role.toString).isEmpty } + def getCustomers(ids: List[String]): List[Customer] = { + val customers = { + for {id <- ids + c = Customer.customerProvider.vend.getCustomerByCustomerId(id) + u <- c + } yield { + u + } + } + customers + } + } diff --git a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index 2aa3f62a3..c01a56199 100644 --- a/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -54,6 +54,8 @@ object JSONFactory1_4_0 { kyc_status: Boolean, last_ok_date: Date) + case class CustomerJSONs(customers: List[CustomerJson]) + case class CustomerFaceImageJson(url : String, date : Date) case class CustomerMessagesJson(messages : List[CustomerMessageJson]) @@ -118,6 +120,12 @@ object JSONFactory1_4_0 { last_ok_date = cInfo.lastOkDate ) + + + } + + def createCustomersJson(customers : List[Customer]) : CustomerJSONs = { + CustomerJSONs(customers.map(createCustomerJson)) } def createCustomerMessageJson(cMessage : CustomerMessage) : CustomerMessageJson = { diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 22bdfdbaf..a534da7ec 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -7,7 +7,7 @@ import code.TransactionTypes.TransactionType import code.api.APIFailure import code.api.util.APIUtil._ import code.api.util.ApiRole._ -import code.api.util.{ApiRole, ErrorMessages} +import code.api.util.{APIUtil, ApiRole, ErrorMessages} import code.api.v1_2_1.OBPAPI1_2_1._ import code.api.v1_2_1.{APIMethods121, AmountOfMoneyJSON => AmountOfMoneyJSON121, JSONFactory => JSONFactory121} import code.api.v1_4_0.JSONFactory1_4_0 @@ -2063,6 +2063,48 @@ trait APIMethods200 { } } + + resourceDocs += ResourceDoc( + getCustomers, + apiVersion, + "getCustomers", + "GET", + "/users/current/customers", + "Get all customers for logged in user", + """Information about the currently authenticated user. + | + |Authentication via OAuth is required.""", + emptyObjectJson, + emptyObjectJson, + emptyObjectJson :: Nil, + false, + false, + false, + List(apiTagCustomer)) + + lazy val getCustomers : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { + case "users" :: "current" :: "customers" :: Nil JsonGet _ => { + user => { + for { + u <- user ?~! ErrorMessages.UserNotLoggedIn + //bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} + customerIds: List[String] <- tryo{UserCustomerLink.userCustomerLink.vend.getUserCustomerLinkByUserId(u.userId).map(x=>x.customerId)} ?~! ErrorMessages.CustomerDoNotExistsForUser + } yield { + val json = JSONFactory1_4_0.createCustomersJson(APIUtil.getCustomers(customerIds)) + successJsonResponse(Extraction.decompose(json)) + } + } + } + } + + + + + + + + + } diff --git a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala index 9d449f0a0..edd428ad7 100644 --- a/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala +++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala @@ -177,7 +177,8 @@ object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations2_0_0.deleteEntitlement, Implementations2_0_0.getAllEntitlements, Implementations2_0_0.elasticSearchWarehouse, - Implementations2_0_0.elasticSearchMetrics + Implementations2_0_0.elasticSearchMetrics, + Implementations2_0_0.getCustomers ) routes.foreach(route => { From 6bb6d854bbfd119cfd4097e75612f947ba3929d7 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Wed, 20 Jul 2016 15:52:59 +0200 Subject: [PATCH 642/702] v2.0.0 privateAccountsAtOneBank does not return links #74 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index a534da7ec..588677f19 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -297,6 +297,9 @@ trait APIMethods200 { true, List(apiTagAccount, apiTagPrivateData)) + apiRelations += ApiRelation(privateAccountsAtOneBank, getCoreAccountById, "detail") + apiRelations += ApiRelation(privateAccountsAtOneBank, privateAccountsAtOneBank, "self") + def privateAccountsAtOneBankResult (bank: Bank, u: User) = { val availableAccounts = bank.nonPublicAccounts(u) From 2e42840be76d282fed15aaebc51ef5c0eeab8264 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Thu, 21 Jul 2016 15:33:22 +0200 Subject: [PATCH 643/702] v2.0.0 privateAccountsAtOneBank does not return links #74 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 588677f19..f7f174bf2 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -297,7 +297,7 @@ trait APIMethods200 { true, List(apiTagAccount, apiTagPrivateData)) - apiRelations += ApiRelation(privateAccountsAtOneBank, getCoreAccountById, "detail") + apiRelations += ApiRelation(privateAccountsAtOneBank, createAccount, "new") apiRelations += ApiRelation(privateAccountsAtOneBank, privateAccountsAtOneBank, "self") From 5d46ff8ed2a11f94467f513945c8db5c6649feba Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Fri, 22 Jul 2016 17:36:25 +0200 Subject: [PATCH 644/702] write test / check coverage for v2.0.0 allAccountsAtOneBank #99 --- .../scala/code/api/v2_0_0/APIMethods200.scala | 2 +- .../scala/code/api/v2_0_0/AccountTest.scala | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/test/scala/code/api/v2_0_0/AccountTest.scala diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index f7f174bf2..1d014acf1 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -269,7 +269,7 @@ trait APIMethods200 { case "banks" :: BankId(bankId) :: "accounts" :: Nil JsonGet json => { user => for{ - bank <- Bank(bankId) + bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} } yield { val availableAccounts = bank.accounts(user) successJsonResponse(bankAccountBasicListToJson(availableAccounts, user)) diff --git a/src/test/scala/code/api/v2_0_0/AccountTest.scala b/src/test/scala/code/api/v2_0_0/AccountTest.scala new file mode 100644 index 000000000..65ac0f7f7 --- /dev/null +++ b/src/test/scala/code/api/v2_0_0/AccountTest.scala @@ -0,0 +1,83 @@ +package code.api.v2_0_0 + +import java.text.SimpleDateFormat + +import code.api.DefaultUsers +import code.api.util.APIUtil.OAuth._ +import code.api.v1_2_1.{AmountOfMoneyJSON => AmountOfMoneyJSON121} +import code.model.BankId +import code.model.dataAccess.MappedBankAccount +import net.liftweb.json.Serialization.write +import net.liftweb.json.JsonAST._ + +class AccountTest extends V200ServerSetup with DefaultUsers { + + val exampleDateString: String = "22/08/2013" + val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy") + val exampleDate = simpleDateFormat.parse(exampleDateString) + + val mockBankId = BankId("testBank1") + val newAccountId1 = "NEW_ACCOUNT_ID_01" + + + override def beforeAll() { + super.beforeAll() + } + + override def afterAll() { + super.afterAll() + MappedBankAccount.bulkDelete_!!() + } + + feature("Assuring that Get all accounts at all banks works as expected - v2.0.0") { + + scenario("We create an account and get accounts as anonymous and then as authenticated user") { + Given("The bank") + val testBank = mockBankId + + Then("We create an private account at the bank") + val accountPutJSON = CreateAccountJSON(obpuser1.userId,"CURRENT", AmountOfMoneyJSON121("EUR", "0")) + val requestPut = (v2_0Request / "banks" / testBank.value / "accounts" / newAccountId1).PUT <@ (user1) + val responsePut = makePutRequest(requestPut, write(accountPutJSON)) + + And("We should get a 200") + responsePut.code should equal(200) + + When("We make the anonymous access request") + val requestGet = (v2_0Request / "accounts").GET + val responseGet = makeGetRequest(requestGet) + + Then("We should get a 200") + responseGet.code should equal(200) + + val isPublic: List[Boolean] = + for { + JObject(o) <- responseGet.body + JField("is_public", JBool(isPublic)) <- o + } yield { + isPublic + } + And("All received accounts have to be public") + isPublic.forall(_ == true) should equal(true) + + When("We make the authenticated access request") + val requestGetAll = (v2_0Request / "accounts").GET <@ (user1) + val responseGetAll = makeGetRequest(requestGetAll) + + Then("We should get a 200") + responseGetAll.code should equal(200) + + val isPublicAll = + for { + obj@JObject(o) <- responseGetAll.body + if (o contains JField("id", JString(newAccountId1))) + JBool(isPublic) <- obj \\ "is_public" + } yield { + isPublic + } + And("The new created account has to be private") + isPublicAll.forall(_ == false) should equal(true) + } + } + +} From 72a431a883ba0354cbe95fdd547f613541b7d8b0 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Fri, 22 Jul 2016 18:20:02 +0200 Subject: [PATCH 645/702] Added invalid consumer key error message. This closes #78 --- src/main/scala/code/api/directlogin.scala | 2 +- src/main/scala/code/api/util/APIUtil.scala | 2 +- src/test/scala/code/api/directloginTest.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index c1e78465b..309df7028 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -231,7 +231,7 @@ object DirectLogin extends RestHelper with Loggable { !registeredApplication(parameters.get("consumer_key").getOrElse(""))) { logger.error("application: " + parameters.get("consumer_key").getOrElse("") + " not found") - message = ErrorMessages.InvalidLoginCredentials + message = ErrorMessages.InvalidConsumerKey httpCode = 401 } else diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index 62985f9a4..7ee184678 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -70,13 +70,13 @@ object ErrorMessages { val DirectLoginMissingParameters = "OBP-20002: These DirectLogin parameters are missing: " val DirectLoginInvalidToken = "OBP-20003: This DirectLogin token is invalid or expired: " - val InvalidLoginCredentials = "OBP-20004: Invalid login credentials. Check username/password." val UserNotFoundById = "OBP-20005: User not found by User Id." val UserDoesNotHaveRole = "OBP-20006: User does not have a role " val UserNotFoundByEmail = "OBP-20007: User not found by email." + val InvalidConsumerKey = "OBP-20008: Invalid Consumer Key." // Resource related messages val BankNotFound = "OBP-30001: Bank not found. Please specify a valid value for BANK_ID." diff --git a/src/test/scala/code/api/directloginTest.scala b/src/test/scala/code/api/directloginTest.scala index 3f397ee89..b9700441c 100644 --- a/src/test/scala/code/api/directloginTest.scala +++ b/src/test/scala/code/api/directloginTest.scala @@ -123,7 +123,7 @@ class directloginTest extends ServerSetup with BeforeAndAfter { Then("We should get a 401 - Unauthorized") response.code should equal(401) - assertResponse(response, ErrorMessages.InvalidLoginCredentials) + assertResponse(response, ErrorMessages.InvalidConsumerKey) } scenario("Login with correct everything!") { From ed604ba4fe125448c4c3b1d08715bb0098205e07 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 24 Jul 2016 12:09:02 +0100 Subject: [PATCH 646/702] TODOs on createCustomer --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 588677f19..c7d192d95 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1564,6 +1564,14 @@ trait APIMethods200 { false, List(apiTagCustomer)) + + + // TODO + // Separate customer creation (keep here) from customer linking (remove from here) + // Remove user_id from CreateCustomerJson + // Logged in user must have CanCreateCustomer (should no longer be able create customer for own user) + // Add ApiLink to createUserCustomerLink + lazy val createCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: BankId(bankId) :: "customers" :: Nil JsonPost json -> _ => { user => @@ -1707,6 +1715,9 @@ trait APIMethods200 { false, List(apiTagUser, apiTagCustomer)) + // TODO + // Allow multiple UserCustomerLinks per user (and bank) + lazy val createUserCustomerLinks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = { case "banks" :: "user_customer_links" :: Nil JsonPost json -> _ => { user => From 15e4c701c60e0f7b75c710cdb6cf5c61d568f276 Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 25 Jul 2016 09:11:35 +0200 Subject: [PATCH 647/702] write test / check coverage for v2.0.0 allAccountsAllBanks #100 --- .../scala/code/api/v2_0_0/AccountTest.scala | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/test/scala/code/api/v2_0_0/AccountTest.scala b/src/test/scala/code/api/v2_0_0/AccountTest.scala index 65ac0f7f7..92eb8514a 100644 --- a/src/test/scala/code/api/v2_0_0/AccountTest.scala +++ b/src/test/scala/code/api/v2_0_0/AccountTest.scala @@ -31,7 +31,7 @@ class AccountTest extends V200ServerSetup with DefaultUsers { feature("Assuring that Get all accounts at all banks works as expected - v2.0.0") { - scenario("We create an account and get accounts as anonymous and then as authenticated user") { + scenario("We create an account and get accounts as anonymous and then as authenticated user - allAccountsAllBanks") { Given("The bank") val testBank = mockBankId @@ -78,6 +78,54 @@ class AccountTest extends V200ServerSetup with DefaultUsers { And("The new created account has to be private") isPublicAll.forall(_ == false) should equal(true) } + + scenario("We create an account and get accounts as anonymous and then as authenticated user - allAccountsAtOneBank") { + Given("The bank") + val testBank = mockBankId + + Then("We create an private account at the bank") + val accountPutJSON = CreateAccountJSON(obpuser1.userId,"CURRENT", AmountOfMoneyJSON121("EUR", "0")) + val requestPut = (v2_0Request / "banks" / testBank.value / "accounts" / newAccountId1).PUT <@ (user1) + val responsePut = makePutRequest(requestPut, write(accountPutJSON)) + + And("We should get a 200") + responsePut.code should equal(200) + + When("We make the anonymous access request") + val requestGet = (v2_0Request / "banks" / testBank.value / "accounts").GET + val responseGet = makeGetRequest(requestGet) + + Then("We should get a 200") + responseGet.code should equal(200) + + val isPublic: List[Boolean] = + for { + JObject(o) <- responseGet.body + JField("is_public", JBool(isPublic)) <- o + } yield { + isPublic + } + And("All received accounts have to be public") + isPublic.forall(_ == true) should equal(true) + + When("We make the authenticated access request") + val requestGetAll = (v2_0Request / "banks" / testBank.value / "accounts").GET <@ (user1) + val responseGetAll = makeGetRequest(requestGetAll) + + Then("We should get a 200") + responseGetAll.code should equal(200) + + val isPublicAll = + for { + obj@JObject(o) <- responseGetAll.body + if (o contains JField("id", JString(newAccountId1))) + JBool(isPublic) <- obj \\ "is_public" + } yield { + isPublic + } + And("The new created account has to be private") + isPublicAll.forall(_ == false) should equal(true) + } } } From ed147687ec66aec7498460cca61c1f6f23bee40d Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 25 Jul 2016 10:54:29 +0200 Subject: [PATCH 648/702] Updated old (removed) sonatype repository link --- mvn.sh | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mvn.sh b/mvn.sh index b1af2234a..74a43ec18 100755 --- a/mvn.sh +++ b/mvn.sh @@ -1,5 +1,5 @@ #!/bin/sh -export MAVEN_OPTS="-Xmx512m -Xms512m -XX:MaxPermSize=256m" +export MAVEN_OPTS="-Xmx512m -Xms512m" mvn $1 $2 $3 $4 diff --git a/pom.xml b/pom.xml index 00e1a4794..72560a2c9 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ scala-tools.releases Scala-Tools Dependencies Repository for Releases - https://oss.sonatype.org/content/groups/scala-tools/ + https://oss.sonatype.org/content/repositories/releases/ java.net.maven3 @@ -227,7 +227,7 @@ once . WDF TestSuite.txt - -Drun.mode=test -XX:MaxPermSize=128m -Xms512m -Xmx512m + -Drun.mode=test -Xms512m -Xmx512m From 60ebd4c65203da9935624e1a4e01932646b03a8f Mon Sep 17 00:00:00 2001 From: Marko Milic Date: Mon, 25 Jul 2016 11:14:07 +0200 Subject: [PATCH 649/702] fix v2.0.0 accountById should allow anon user if view_id requested is public. #102 --- src/main/scala/code/api/v2_0_0/APIMethods200.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 633edf2e6..43c80a4c5 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -895,11 +895,11 @@ trait APIMethods200 { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "account" :: Nil JsonGet json => { user => for { - u <- user ?~! ErrorMessages.UserNotLoggedIn // TODO. Allow anon for public views. (check previous version of this call) Check we have a user (rather than error or empty) bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound} // Check bank exists. account <- BankAccount(bank.bankId, accountId) ?~! {ErrorMessages.AccountNotFound} // Check Account exists. availableViews <- Full(account.permittedViews(user)) view <- View.fromUrl(viewId, account) ?~! {ErrorMessages.ViewNotFound} + canUserAccessView <- tryo(availableViews.find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId} moderatedAccount <- account.moderatedBankAccount(view, user) } yield { val viewsAvailable = availableViews.map(JSONFactory121.createViewJSON).sortBy(_.short_name) From 0626fefd7c990271566822097afd020135cf28cd Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 25 Jul 2016 11:25:35 +0200 Subject: [PATCH 650/702] Reverted pom.xml and mv.sh (added back removed -XX:MaxPermSize), just in case someone else is still using java 1.7 instead of 1.8 --- mvn.sh | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mvn.sh b/mvn.sh index 74a43ec18..fab959330 100755 --- a/mvn.sh +++ b/mvn.sh @@ -1,5 +1,5 @@ #!/bin/sh -export MAVEN_OPTS="-Xmx512m -Xms512m" +export MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=256m -Xms512m" mvn $1 $2 $3 $4 diff --git a/pom.xml b/pom.xml index 72560a2c9..35f7e7e28 100644 --- a/pom.xml +++ b/pom.xml @@ -227,7 +227,7 @@ once . WDF TestSuite.txt - -Drun.mode=test -Xms512m -Xmx512m + -Drun.mode=test -XX:MaxPermSize=128m -Xms512m -Xmx512m From 9e82de7691dcacd62f46e2860b11ae5817f7def0 Mon Sep 17 00:00:00 2001 From: Petar Bozin Date: Mon, 25 Jul 2016 12:00:49 +0200 Subject: [PATCH 651/702] Fixed redirection when logging in from external application --- .../scala/code/model/dataAccess/OBPUser.scala | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 2c03395f0..075a46478 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -381,23 +381,31 @@ import net.liftweb.util.Helpers._ // If not found locally, try to authenticate user via Kafka, if enabled in props if (Props.get("connector").openOrThrowException("no connector set") == "kafka") { val preLoginState = capturePreLoginState() - val extUser = for { + info("login redir: " + loginRedirect.get) + val redir = loginRedirect.get match { + case Full(url) => + loginRedirect(Empty) + url + case _ => + homePage + } + for { username_ <- S.param("username") password_ <- S.param("password") user_ <- getUserFromKafka(username_, password_) } yield { if (user != null) { logUserIn(user_, () => { + for { + u <- APIUser.find(By(APIUser.email, user_.email)) + v <- tryo { + KafkaMappedConnector.updateUserAccountViews(u) + } + } S.notice(S.?("logged.in")) preLoginState() - S.redirectTo(homePage) + S.redirectTo(redir) }) - for { - u <- APIUser.find(By(APIUser.email, user_.email)) - v <- tryo { - KafkaMappedConnector.updateUserAccountViews(u) - } - } user_ } else From 592cf923927e01b7301a941bfa224412d49fc476 Mon Sep 17 00:00:00 2001 From: Sebastian Henschel Date: Mon, 25 Jul 2016 16:26:21 +0200 Subject: [PATCH 652/702] More CSS updates to landing page --- src/main/webapp/index.html | 26 +++--- src/main/webapp/media/css/website.css | 88 +++++++++++-------- src/main/webapp/templates-hidden/default.html | 5 +- 3 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 8f7cc35bc..e473d7e4c 100755 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -82,7 +82,7 @@ Berlin 13359, Germany
    - @@ -176,7 +176,7 @@ Berlin 13359, Germany
    -
    + @@ -187,50 +187,50 @@ Berlin 13359, Germany
    python

    Python

    - By OpenBankProject + By OpenBankProject
    django

    Django

    - By Azd325 + By OpenBankProject
    nodejs

    NodeJS

    - By OpenBankProject + By OpenBankProject
    mac

    Mac

    - By T0rst + By OpenBankProject
    ios

    IOS

    - By T0rst + By OpenBankProject
    android

    Android

    - By OpenBankProject + By OpenBankProject
    scala

    Scala (liftweb)

    - By OpenBankProject + By OpenBankProject
    php

    PHP

    - By Solonas + By Solonas
    csharp

    C#

    - By Sweechem
    (this has a nice OAuth walk through with C# snippets) + By Sweechem
    -

    Please make sure you are using the correct sandbox domain in all calls when using the SDKs. In doubt, drop us a line.

    +

    Please make sure you are using the correct sandbox domain when using the SDKs. In doubt, drop us a line.

    @@ -312,7 +312,7 @@ Berlin 13359, Germany

    Get started building your application using this sandbox now, register for a developer key

    - diff --git a/src/main/webapp/media/css/website.css b/src/main/webapp/media/css/website.css index 9d64c21e2..d5b723ae4 100644 --- a/src/main/webapp/media/css/website.css +++ b/src/main/webapp/media/css/website.css @@ -1,14 +1,15 @@ @import url('reset.css'); -body { - font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; - font-weight: 400; -} - html { height: 100% } + +body, div, h1, h2, h3, h4, p, span { + font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; + font-weight: 400; +} + a { color: #666666; text-decoration: none; @@ -211,7 +212,10 @@ input.submit { } #main-about p.about-text a { - color: lightgray; + color: white; +} +#main-about p.about-text a:hover { + text-decoration: underline; } #main-links { @@ -228,6 +232,7 @@ input.submit { border: 2px solid gray; border-radius: 25px; margin: 30px 20px 0px 20px; + font-size: 24px; } #main-sandbox { @@ -239,7 +244,7 @@ input.submit { #main-sandbox h1 { color: white; - font-size: 35px; + font-size: 36px; margin-bottom: 15px; margin-left: -5px; margin-top: 15px; @@ -248,7 +253,7 @@ input.submit { #main-get_started { text-align: left; - margin: 40px auto; + margin: 50px auto; background-color: #f1f4f8; } @@ -286,7 +291,7 @@ input.submit { #main-get_started h1 { text-align: center; font-weight: bold; - font-size: 35px; + font-size: 36px; margin: 30px 0 30px 0; display: table-caption; } @@ -297,13 +302,14 @@ input.submit { color: #333333; } -.main-get_api_key { - padding: 30px 0; +#main-get_started .green-button { + padding-bottom: 40px; } + #main-apis { text-align: left; - margin: 40px auto; + margin: 50px auto; display: table; } .main-apis-row { @@ -330,8 +336,8 @@ input.submit { #main-apis h1 { text-align: center; font-weight: bold; - font-size: 35px; - margin: 30px 0 30px 0; + font-size: 36px; + margin: 0px 0 30px 0; display: table-caption; } #main-apis h2 { @@ -340,19 +346,6 @@ input.submit { margin-top: 13px; } -#main-apiexplorer, .main-get_api_key { - width: 300px; - margin: 30px auto; - text-align: center; -} -#main-apiexplorer a, .main-get_api_key a { - border: 3px solid green; - border-radius: 25px; - color: green; - padding: 10px; - font-size: 24px; -} - #main-showcases { margin: 50px auto; @@ -370,24 +363,24 @@ input.submit { font-size: 24px; } -#main-showcases p { - padding: 0 10px; -} - .main-showcases-item { display: inline-block; margin: 30px 10px; width: 230px; + font-size: small; } -.main-showcases-item span { - font-size: small; +.main-showcases-item a { + color: white; +} +.main-showcases-item a:hover { + text-decoration: underline; } #main-faq { text-align: left; - margin: 40px auto; + margin: 50px auto; padding:0 40px; background-color: #f1f4f8; } @@ -401,7 +394,7 @@ input.submit { } #main-faq h2 { - font-size: 18px; + font-size: 18px; /* text won't fit on line with 24px */ font-weight: bold; color: #333333; } @@ -447,8 +440,8 @@ input.submit { #main-start_building { - margin: 40px 0; - padding: 40px 80px; + margin: 50px 0; + padding: 0 80px; } #main-start_building h1 { font-size: 36px; @@ -458,7 +451,7 @@ input.submit { #main-partners { - margin: 40px auto 0 auto; + margin: 50px auto; text-align: center; } @@ -499,6 +492,21 @@ input.submit { +.green-button { + width: 300px; + margin: 30px auto; + text-align: center; +} +.green-button a { + border: 3px solid green; + border-radius: 25px; + color: green; + padding: 10px; + font-size: 24px; +} + + + @@ -630,7 +638,6 @@ input.submit { width: inherit; text-transform: uppercase; font-weight: 500; - font-family: inherit; } .account-in-content form.login .field .forgot { @@ -921,6 +928,9 @@ span.alias_indicator_private { #footer a { color: white; } +#footer #copyright { + margin-bottom: 10px; +} #authorizeSection { diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html index a8af38053..7a87b7ea6 100755 --- a/src/main/webapp/templates-hidden/default.html +++ b/src/main/webapp/templates-hidden/default.html @@ -114,9 +114,8 @@ Berlin 13359, Germany The main content gets bound here