diff --git a/.gitignore b/.gitignore
index 49c74b4ba..4e8dc6fcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,4 @@
target
src/main/resources/
src/test/resources/
-*.iml
\ No newline at end of file
+*.iml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..d3bec8638
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,32 @@
+# Contributing
+
+
+## Hello!
+
+Thank you for your interest in contributing to the Open Bank Project!
+
+## Pull requests
+
+If submitting a pull request please read and sign our [CLA](http://github.com/OpenBankProject/OBP-API/blob/develop/Harmony_Individual_Contributor_Assignment_Agreement.txt) and send it to contact@tesobe.com - We'll send you back a code to include in the comment section of subsequent pull requests.
+
+Please reference Issue Numbers in your commits.
+
+## Code comments
+
+Please comment your code ! :-) Imagine an engineer is trying to fix a production issue: she is working on a tiny screen, via a dodgy mobile Internet connection, in a sandstorm - Your code is fresh in your mind. Your comments could help her!
+
+## Issues
+
+If would like to report an issue or suggest any kind of improvement please use Github Issues.
+
+## Licenses
+
+Open Bank Project API, API Explorer and Sofi are dual licenced under the AGPL and commercial licenses. Open Bank Project SDKs are licenced under Apache 2 or MIT style licences.
+
+Please see the NOTICE for each project licence.
+
+## Setup and Tests
+
+See the README for instructions on setup and running the tests :-)
+
+Welcome!
\ No newline at end of file
diff --git a/Harmony_Individual_Contributor_Assignment_Agreement.txt b/Harmony_Individual_Contributor_Assignment_Agreement.txt
index 26226395f..808e12fcd 100644
--- a/Harmony_Individual_Contributor_Assignment_Agreement.txt
+++ b/Harmony_Individual_Contributor_Assignment_Agreement.txt
@@ -82,22 +82,31 @@ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR US BE
-You
________________________
-Name: ________________________
-Address: ________________________
- ________________________
-Us
+You:
+
+
+Name: ________________________________
+
+Address: ________________________________
+
+Github username ________________________________
+
+Email / Phone ________________________________
+
+
________________________
+
+Us:
+
Name: Simon Redfern
-Title: CEO TESOBE / Music Pictures Ltd
-Address: Osloerstrasse 16/17
- Berlin 13359, German
-
+Title: CEO, TESOBE Ltd
+Address: Osloerstrasse 16/17, Berlin 13359, Germany
+________________________
[SUBMISSION_INSTRUCTIONS] – In person.
@@ -125,4 +134,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.
diff --git a/NOTICE b/NOTICE
index d05773491..c0f87136b 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,5 @@
-Open Bank Project - Transparency / Social Finance Web Application
-Copyright (C) 2011, 2012, TESOBE / Music Pictures Ltd
+Open Bank Project API
+Copyright (C) 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
@@ -15,14 +15,14 @@ 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
+TESOBE 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
+ Simon Redfern
+ Stefan Bethge
+ Everett Sochowski
+ Ayoub Benali
diff --git a/README.md b/README.md
index a090ba3e5..cfbfcd238 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
@@ -22,12 +22,11 @@ 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
-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
@@ -35,11 +34,111 @@ 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
+
+## To run with IntelliJ IDEA
+
+* Make sure you have the IntelliJ Scala plugin installed.
+
+* Create a new folder e.g. OpenBankProject and cd there
+
+* git clone https://github.com/OpenBankProject/OBP-API.git
+
+* In IntelliJ IDEA do File -> New -> Project from existing sources
+
+* (Alternatively you can do File -> New -> Project from VCS and checkout from github)
+
+* 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.11.8 or so.
+
+* In src/main/resources/props create a test.default.props for tests. Set connector=mapped
+
+* In src/main/resources/props create a .default.props for development. Set connector=mapped
+
+* Now **Rebuild** the project so everything is compiled.
+
+* Run RunWebApp by right clicking on it or selecting Run. The built in jetty server should start on localhost:8080
+
+* Browse to localhost:8080 but don't try anything else there yet.
+
+### Run some tests.
+
+* Run a single test. For instance right click on test/scala/code/branches/MappedBranchProviderTest and select Run Mapp...
+
+* 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
+ 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.
+
+ Note: You may want to disable some tests not relevant to your setup e.g.:
+ set bank_account_creation_listener=false in test.default.props
+
+
+## Other ways to run tests
+
+* See pom.xml for test configuration
+* See http://www.scalatest.org/user_guide
+
+
+## From the command line
+
+Set memory options
+export MAVEN_OPTS="-Xmx3000m -XX:MaxPermSize=512m"
+
+Run one test
+mvn -DwildcardSuites=code.api.directloginTest test
+
----
-# Databases:
+## Ubuntu
-The default datastores used are MongoDB (metadata, transaction cache) and Postgres (user accounts).
+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).
+
+## 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):
+
+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
+
+
+## Scala / Lift
+
+* We use scala and liftweb http://www.liftweb.net/
+
+* Advanced architecture: http://exploring.liftweb.net/master/index-9.html
+
+* A good book on Lift: "Lift in Action" by Timothy Perrett published by Manning.
diff --git a/mvn.sh b/mvn.sh
new file mode 100755
index 000000000..011163582
--- /dev/null
+++ b/mvn.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+# Deprecated option -XX:MaxPermSize=256m is kept
+# just in case someone still uses java 1.7
+export MAVEN_OPTS="-Xmx1024m -Xms1024m -Xss1024k -XX:MaxPermSize=256m"
+
+mvn $1 $2 $3 $4
diff --git a/pom.xml b/pom.xml
index 2888e0fe9..192e5b2af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,9 +11,9 @@
Open Bank Project API2011
- 2.10
- 2.10.4
- 2.6-M4
+ 2.11
+ 2.11.8
+ 2.6.3UTF-8${project.build.sourceEncoding}
@@ -27,12 +27,12 @@
scala-tools.releasesScala-Tools Dependencies Repository for Releases
- https://oss.sonatype.org/content/groups/scala-tools/
+ https://oss.sonatype.org/content/repositories/releases/
- 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
@@ -50,6 +50,11 @@
+
+ org.scala-lang.modules
+ scala-xml_${scala.version}
+ 1.0.5
+ net.liftweblift-mapper_${scala.version}
@@ -58,17 +63,17 @@
net.databinder.dispatchdispatch-lift-json_${scala.version}
- 0.10.1
+ 0.11.3net.databinder.dispatchdispatch-core_${scala.version}
- 0.10.1
+ 0.11.3
- ch.qos.logback
- logback-classic
- 1.0.9
+ org.apache.kafka
+ kafka_${scala.version}
+ 0.10.0.0org.bouncycastle
@@ -76,44 +81,50 @@
1.46
- postgresql
+ org.postgresqlpostgresql
- 8.4-701.jdbc4
+ 9.4-1206-jdbc4com.h2databaseh2
- 1.2.138
+ 1.4.191runtimejavax.servlet
- servlet-api
- 2.5
+ javax.servlet-api
+ 3.1.0providedjunitjunit
- 4.9
+ 4.12testorg.scalatestscalatest_${scala.version}
- 2.0
+ 2.2.6testorg.eclipse.jettyjetty-server
- 7.6.16.v20140903
+ 9.2.15.v20160210testorg.eclipse.jettyjetty-webapp
- 7.6.16.v20140903
+ 9.2.15.v20160210
+ test
+
+
+ net.manub
+ scalatest-embedded-kafka_${scala.version}
+ 0.7.1test
@@ -129,17 +140,74 @@
org.seleniumhq.seleniumselenium-java
- 2.35.0
+ 2.52.0
+
+
+ org.seleniumhq.selenium
+ selenium-htmlunit-driver
+ 2.52.0
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.2
+
+
+ net.sourceforge.htmlunit
+ htmlunit
+ 2.21com.rabbitmqamqp-client
- 3.2.0
+ 3.6.1net.liftmodulesamqp_2.6_${scala.version}
- 1.3
+ 1.4-SNAPSHOT
+
+
+ org.pegdown
+ pegdown
+ 1.6.0
+
+
+ com.jason-goodwin
+ authentikat-jwt_${scala.version}
+ 0.4.1
+
+
+ com.tokbox
+ opentok-server-sdk
+ 3.0.0-beta.1
+
+
+ org.elasticsearch
+ elasticsearch
+ 2.3.0
+
+
+ com.sksamuel.elastic4s
+ 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
@@ -150,7 +218,7 @@
org.apache.maven.pluginsmaven-surefire-plugin
- 2.9
+ ${scala.version}true
@@ -159,12 +227,13 @@
org.scalatestscalatest-maven-plugin
- 1.0-RC1
+ 1.0${project.build.directory}/surefire-reports
+ once.WDF TestSuite.txt
- -Drun.mode=test
+ -Drun.mode=test -XX:MaxPermSize=128m -Xms512m -Xmx512m
@@ -179,7 +248,7 @@
org.codehaus.mojobuild-helper-maven-plugin
- 1.9.1
+ 1.10generate-sources
@@ -197,14 +266,16 @@
net.alchim31.mavenscala-maven-plugin
- 3.2.0
+ 3.2.2
- ${scala.compiler}${project.build.sourceEncoding}
- -Xmx1024m-DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties
+ ${scala.compiler}
+ ${scala.version}
+ incremental
+ true
@@ -212,18 +283,33 @@
compiletestCompile
+
+
+ -dependencyfile
+ ${project.build.directory}/.scala_dependencies
+ -Xmax-classfile-name
+ 78
+
+
+
+
+ scala-test-compile
+ process-test-resources
+
+ testCompile
+ org.apache.maven.pluginsmaven-war-plugin
- 2.3
+ 2.6org.apache.maven.pluginsmaven-resources-plugin
- 2.5
+ 3.0.1default-copy-resources
@@ -250,16 +336,17 @@
org.mortbay.jettymaven-jetty-plugin
- 6.1.9
+ 6.1.26/5
+ -Xmx512m -Xms512mnet.alchim31.mavenyuicompressor-maven-plugin
- 1.3.2
+ 1.5.1
@@ -275,7 +362,7 @@
org.apache.maven.pluginsmaven-idea-plugin
- 2.2
+ 2.2.1true
@@ -283,7 +370,7 @@
org.apache.maven.pluginsmaven-eclipse-plugin
- 2.7
+ 2.10true
@@ -301,7 +388,7 @@
pl.project13.mavengit-commit-id-plugin
- 2.1.0
+ 2.2.1
@@ -323,14 +410,16 @@
net.alchim31.mavenscala-maven-plugin
- 3.2.0
+ 3.2.2
- ${scala.compiler}${project.build.sourceEncoding}
- -Xmx1024m-DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties
+ ${scala.version}
+ ${scala.compiler}
+ incremental
+ true
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..0082a8394 100755
--- a/project/build.scala
+++ b/project/build.scala
@@ -1,29 +1,23 @@
-
-
import sbt._
import Keys._
-import com.github.siasia._
-import PluginKeys._
-import WebPlugin._
-import WebappPlugin._
+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
)
- def yourWebSettings = webSettings ++ Seq(
- // If you are use jrebel
- scanDirectories in Compile := Nil
- )
-
- lazy val opanBank = Project(
+ lazy val openBank = Project(
pom.artifactId,
base = file("."),
- settings = defaultSettings ++ yourWebSettings ++ pom.settings)
+ settings = defaultSettings ++ pom.settings)
+ .enablePlugins(JettyPlugin)
object pom {
@@ -66,7 +60,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
@@ -76,7 +70,8 @@ object LiftProjectBuild extends Build {
lazy val settings = Seq(
scalaVersion := pomScalaVersion,
libraryDependencies ++= pomDeps,
- resolvers ++= pomRepos
+ resolvers ++= pomRepos,
+ containerPort := 8080
)
}
@@ -85,12 +80,12 @@ 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"),
// 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
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/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;
+ }
+
+
+}
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
diff --git a/src/main/resources/props/sample.props.template b/src/main/resources/props/sample.props.template
index 284d1142b..3d8a7c475 100644
--- a/src/main/resources/props/sample.props.template
+++ b/src/main/resources/props/sample.props.template
@@ -1,24 +1,197 @@
#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
+### OBP-API configuration
+
+
+### base configuration
+
+#which data connector to use
+connector=mapped
+#connector=mongodb
+#connector=kafka
+#connector=kafka_lib_v0
+#connector=...
+
+#if using kafka connector, set zookeeper host
+#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.request_topic=Request
+#kafka.response_topic=Response
+
+
+#ElasticSearch
+#allow_elasticsearch=true
+#allow_elasticsearch_warehouse=true
+#allow_elasticsearch_metrics=true
+
+#ElasticSearch warehouse
+#es.warehouse.index=warehouse
+#es.warehouse.host=localhost
+#es.warehouse.port.tcp=9300
+#es.warehouse.port.http=9200
+
+#ElasticSearch metrics
+#es.metrics.index=metrics
+#es.metrics.host=localhost
+#es.metrics.port.tcp=9300
+#es.metrics.port.http=9200
+
+
#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.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
+#our own remotely accessible URL
#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 a URL)
hostname=http://127.0.0.1:8080
-#set this to false if you don't want the api payments call to work
-payments_enabled=true
+#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=8080
-#mail server config: not need in dev mode, but important for production
+
+#The start of the api path (before the version)
+#It is *strongly* recommended not to change this - since Apps will be expecting the api at /obp/+version
+#Including it here so we have a canonical source of the value
+#This was introduced March 2016, some code might use hardcoded value instead.
+#Default value is obp (highly recomended)
+apiPathZero=obp
+
+#sending mail out
+#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
mail.smtp.host=127.0.0.1
mail.smtp.port=25
+#oauth token timeout
+token_expiration_weeks=4
+
+
+
+### sandbox
+
+#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
+
+
+
+### api features
+
+#secret key that allows access to the "add cash transactions" api. You should change this to your own secret key
+cashApplicationKey=change_me
+
+#set this to false if you don't want the api payments call to work (starting with v1.2.1)
+payments_enabled=true
+
+#transaction requests are replacing simple payments starting from 1.4.0
+transactionRequests_enabled=true
+transactionRequests_connector=mapped
+transactionRequests_supported_types=SANDBOX_TAN,INTRABANK,SEPA,FREE_FORM
+
+
+# For video conference meetings (createMeeting)
+meeting.tokbox_enabled=false
+meeting.tokbox_api_key=changeme
+meeting.tokbox_api_secret=changeme
+
+
+
+### management modules
+
+#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 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 send a message to the hbci project to refresh transactions for an account
+messageQueue.updateBankAccountsTransaction=false
+
+#the minimum time between updates in hours
+messageQueue.updateTransactionsInterval=1
+
+#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 (local ones like HBCI connected)
+allow_account_deletion=true
+
#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=change_me
+
+
+
+### web interface configuration
+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
+webui_index_page_about_section_text =
\
+ Welcome to the API Sandbox powered by the Open Bank Project! \
+
+
+
+# API Explorer url. Change to your instance
+webui_api_explorer_url = http://apiexplorer.openbankproject.com
+
+# Sofi url. (AKA Social Finance) Change to your instance
+webui_sofi_url = http://sofi.openbankproject.com
+
+# Starting page of documentation. Change this if you have a specific landing page.
+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
+
+
+
+## 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"}]
+
+# 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 =
+
+
+## API Options
+apiOptions.getBranchesIsPublic = true
+apiOptions.getAtmsIsPublic = true
+apiOptions.getProductsIsPublic = true
+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/resources/props/test.default.props.template b/src/main/resources/props/test.default.props.template
new file mode 100644
index 000000000..61524b5b2
--- /dev/null
+++ b/src/main/resources/props/test.default.props.template
@@ -0,0 +1,83 @@
+#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
+
+####################################
+## Minimum Settings
+
+#which data connector to use
+
+#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
+
+#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.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)
+# --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
+
+
+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
+
+#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
diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala
index eed2938ce..799f10901 100755
--- a/src/main/scala/bootstrap/liftweb/Boot.scala
+++ b/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -1,6 +1,6 @@
/**
Open Bank Project - API
-Copyright (C) 2011, 2013, TESOBE / Music Pictures Ltd
+Copyright (C) 2011-2016, 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
@@ -31,32 +31,49 @@ 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.customer.{MappedCustomer, MappedCustomerMessage}
+import code.entitlement.MappedEntitlement
+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
-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.bankbranches.{MappedBankBranch, MappedDataLicense}
-import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo}
-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.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.transaction_types.MappedTransactionType
import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks}
+import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest}
+import code.usercustomerlinks.MappedUserCustomerLink
+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, _}
+import code.api.Constant._
+import code.transaction.MappedTransaction
+
/**
* A class that's instantiated early and run. It allows the application
@@ -64,8 +81,6 @@ import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks}
*/
class Boot extends Loggable{
def boot {
-
-
val contextPath = LiftRules.context.path
val propsPath = tryo{Box.legacyNullTest(System.getProperty("props.resource.dir"))}.toIterable.flatten
@@ -124,23 +139,19 @@ class Boot extends Loggable{
firstChoicePropsDir.flatten.toList ::: secondChoicePropsDir.flatten.toList
}
-
- // This sets up MongoDB config
- 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,
@@ -154,6 +165,15 @@ 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" ||
+ Props.get("connector").getOrElse("") == "kafka" )
+ 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"
@@ -163,27 +183,50 @@ class Boot extends Loggable{
}
logger.info("running mode: " + runningMode)
+ logger.info(s"ApiPathZero (the bit before version) is $ApiPathZero")
-
- // ensure our relational database's tables are created/fit the schema
- schemifyAll()
-
- // where to search snippet
+ // where to search snippets
LiftRules.addToPackages("code")
+ //OAuth API call
+ LiftRules.statelessDispatch.append(OAuthHandshake)
+
+ // JWT auth endpoints
+ if(Props.getBool("allow_direct_login", true)) {
+ LiftRules.statelessDispatch.append(DirectLogin)
+ }
+
// Add the various API versions
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)
+ LiftRules.statelessDispatch.append(v2_0_0.OBPAPI2_0_0)
+ LiftRules.statelessDispatch.append(v2_1_0.OBPAPI2_1_0)
+
+ //add management apis
+ LiftRules.statelessDispatch.append(ImporterAPI)
+ LiftRules.statelessDispatch.append(AccountsAPI)
// add other apis
LiftRules.statelessDispatch.append(BankMockAPI)
- // LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry bellow
- //OAuth API call
- LiftRules.statelessDispatch.append(OAuthHandshake)
+
+
+
+ // 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
+ if(Props.getBool("allow_sandbox_data_import", false)) {
+ LiftRules.statelessDispatch.append(SandboxApiCalls)
+ } else {
+ logger.info("Not adding sandbox api calls")
+ }
//launch the scheduler to clean the database from the expired tokens and nonces
Schedule.schedule(()=> OAuthAuthorisation.dataBaseCleaner, 2 minutes)
@@ -191,18 +234,28 @@ class Boot extends Loggable{
val accountCreation = {
if(Props.getBool("allow_sandbox_account_creation", false)){
//user must be logged in, as a created account needs an owner
- List(Menu("Sandbox Account Creation", "Create Sandbox Test Account") / "create-sandbox-account" >> OBPUser.loginFirst)
+ // Not mentioning test and sandbox for App store purposes right now.
+ List(Menu("Sandbox Account Creation", "Create Bank Account") / "create-sandbox-account" >> OBPUser.loginFirst)
} else {
Nil
}
}
+ // API Metrics (logs of API calls)
+ // If set to true we will write each URL with params to a datastore / log file
+ if (Props.getBool("write_metrics", false)) {
+ logger.info("writeMetrics is true. We will write API metrics")
+ } else {
+ logger.info("writeMetrics is false. We will NOT write API metrics")
+ }
+
+
// Build SiteMap
val sitemap = List(
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" >> 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
@@ -230,15 +283,35 @@ class Boot extends Loggable{
// What is the function to test if a user is logged in?
LiftRules.loggedInTest = Full(() => OBPUser.loggedIn_?)
+ // Template(/Response?) encoding
+ LiftRules.early.append(_.setCharacterEncoding("utf-8"))
+
// Use HTML5 for rendering
LiftRules.htmlProperties.default.set((r: Req) =>
new Html5Properties(r.userAgent))
LiftRules.explicitlyParsedSuffixes = Helpers.knownSuffixes &~ (Set("com"))
+ //set base localization to english (instead of computer default)
+ Locale.setDefault(Locale.ENGLISH)
+
+ //override locale calculated from client request with default (until we have translations)
+ LiftRules.localeCalculator = {
+ case fullReq @ Full(req) => Locale.ENGLISH
+ case _ => Locale.ENGLISH
+ }
+
// Make a transaction span the whole HTTP request
S.addAround(DB.buildLoanWrapper)
+ try {
+ val useMessageQueue = Props.getBool("messageQueue.createBankAccounts", false)
+ if(useMessageQueue)
+ 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)
})
@@ -276,8 +349,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)
@@ -313,11 +386,42 @@ class Boot extends Loggable{
}
object ToSchemify {
- val models = List(OBPUser, Admin, Nonce, Token, Consumer,
- ViewPrivileges, ViewImpl, APIUser, MappedAccountHolder,
- MappedComment, MappedNarrative, MappedTag,
- MappedTransactionImage, MappedWhereTag, MappedCounterpartyMetadata,
- MappedCounterpartyWhereTag, MappedBank, MappedBankAccount, MappedTransaction,
- MappedMetric, MappedCustomerInfo, MappedCustomerMessage,
- MappedBankBranch, MappedDataLicense)
+ val models = List(OBPUser,
+ Admin,
+ Nonce,
+ Token,
+ Consumer,
+ ViewPrivileges,
+ ViewImpl,
+ APIUser,
+ MappedAccountHolder,
+ MappedComment,
+ MappedNarrative,
+ MappedTag,
+ MappedWhereTag,
+ MappedCounterpartyMetadata,
+ MappedCounterpartyWhereTag,
+ MappedBank,
+ MappedBankAccount,
+ MappedTransaction,
+ MappedTransactionRequest,
+ MappedTransactionRequest210,
+ MappedTransactionImage,
+ MappedMetric,
+ MappedCustomer,
+ MappedCustomerMessage,
+ MappedBranch,
+ MappedAtm,
+ MappedProduct,
+ MappedCrmEvent,
+ MappedKycDocument,
+ MappedKycMedia,
+ MappedKycCheck,
+ MappedKycStatus,
+ MappedSocialMedia,
+ MappedTransactionType,
+ MappedMeeting,
+ MappedUserCustomerLink,
+ MappedKafkaBankAccountData,
+ MappedEntitlement)
}
diff --git a/src/main/scala/code/api/OBPAPI1.0.scala b/src/main/scala/code/api/OBPAPI1.0.scala
index 624983d2b..9ef47db78 100644
--- a/src/main/scala/code/api/OBPAPI1.0.scala
+++ b/src/main/scala/code/api/OBPAPI1.0.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
@@ -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/OBPAPI1.1.scala b/src/main/scala/code/api/OBPAPI1.1.scala
index 8ff166421..7f9f76553 100644
--- a/src/main/scala/code/api/OBPAPI1.1.scala
+++ b/src/main/scala/code/api/OBPAPI1.1.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
@@ -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))
@@ -642,7 +643,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
("comments" -> comments.map(commentToJson))
}
- def commentsResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
+ def commentsResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
val comments = for {
metadata <- moderatedTransactionMetadata(bankId,accountId,viewId,transactionId,user)
comments <- Box(metadata.comments) ?~ {"view " + viewId + " does not authorize comments access"}
@@ -661,13 +662,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode,oAuthParameters.get("oauth_token"))
- commentsResponce(bankId, accountId, viewId, transactionId, user)
+ commentsResponse(bankId, accountId, viewId, transactionId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, 400)
}
else
- commentsResponce(bankId, accountId, viewId, transactionId, None)
+ commentsResponse(bankId, accountId, viewId, transactionId, None)
}
})
serve("obp" / "v1.1" prefix{
@@ -736,7 +737,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
("tags" -> tags.map(tagToJson))
}
- def tagsResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
+ def tagsResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
val tags = for {
metadata <- moderatedTransactionMetadata(bankId,accountId,viewId,transactionId,user)
tags <- Box(metadata.tags) ?~ {"view " + viewId + " does not authorize tags access"}
@@ -755,13 +756,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode,oAuthParameters.get("oauth_token"))
- tagsResponce(bankId, accountId, viewId, transactionId, user)
+ tagsResponse(bankId, accountId, viewId, transactionId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, 400)
}
else
- tagsResponce(bankId, accountId, viewId, transactionId, None)
+ tagsResponse(bankId, accountId, viewId, transactionId, None)
}
})
serve("obp" / "v1.1" prefix {
@@ -838,7 +839,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
("images" -> images.map(imageToJson))
}
- def imagesResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
+ def imagesResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
val images = for {
metadata <- moderatedTransactionMetadata(bankId,accountId,viewId,transactionId,user)
images <- Box(metadata.images) ?~ {"view " + viewId + " does not authorize tags access"}
@@ -857,13 +858,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode,oAuthParameters.get("oauth_token"))
- imagesResponce(bankId, accountId, viewId, transactionId, user)
+ imagesResponse(bankId, accountId, viewId, transactionId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, 400)
}
else
- imagesResponce(bankId, accountId, viewId, transactionId, None)
+ imagesResponse(bankId, accountId, viewId, transactionId, None)
}
})
serve("obp" / "v1.1" prefix {
@@ -921,7 +922,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def whereTagResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
+ def whereTagResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
val whereTag = for {
metadata <- moderatedTransactionMetadata(bankId,accountId,viewId,transactionId,user)
whereTag <- Box(metadata.whereTag) ?~ {"view " + viewId + " does not authorize tags access"}
@@ -940,13 +941,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- whereTagResponce(bankId, accountId, viewId, transactionId, user)
+ whereTagResponse(bankId, accountId, viewId, transactionId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, 400)
}
else
- whereTagResponce(bankId, accountId, viewId, transactionId, None)
+ whereTagResponse(bankId, accountId, viewId, transactionId, None)
}
})
serve("obp" / "v1.1" prefix{
@@ -1071,7 +1072,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
("swift_bic" -> otherAccount.swift_bic.getOrElse(""))
}
- def otherAccountResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
+ def otherAccountResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, transactionId : TransactionId, user : Box[User]) : JsonResponse = {
moderatedTransactionOtherAccount(bankId,accountId,viewId,transactionId,user) match {
case Full(otherAccount) => JsonResponse(otherAccountToJson(otherAccount), Nil, Nil, 200)
case Failure(msg,_,_) => JsonResponse(Extraction.decompose(ErrorMessage(msg)), Nil, Nil, 400)
@@ -1085,13 +1086,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- otherAccountResponce(bankId, accountId, viewId, transactionId, user)
+ otherAccountResponse(bankId, accountId, viewId, transactionId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, 400)
}
else
- otherAccountResponce(bankId, accountId, viewId, transactionId, None)
+ otherAccountResponse(bankId, accountId, viewId, transactionId, None)
}
})
serve("obp" / "v1.1" prefix {
@@ -1108,7 +1109,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
("physical_location" -> metadata.physicalLocation.map(l => geoTagToJson("physical_location",l)))
}
- def otherAccountMetadataResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, other_account_ID : String, user : Box[User]) : JsonResponse = {
+ def otherAccountMetadataResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, other_account_ID : String, user : Box[User]) : JsonResponse = {
val otherAccountMetaData = for{
otherAccount <- moderatedOtherAccount(bankId, accountId, viewId, other_account_ID, user)
metaData <- Box(otherAccount.metadata) ?~! {"view " + viewId + "does not allow other account metadata access" }
@@ -1127,13 +1128,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- otherAccountMetadataResponce(bankId, accountId, viewId, other_account_ID, user)
+ otherAccountMetadataResponse(bankId, accountId, viewId, other_account_ID, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, 400)
}
else
- otherAccountMetadataResponce(bankId, accountId, viewId, other_account_ID, None)
+ otherAccountMetadataResponse(bankId, accountId, viewId, other_account_ID, None)
}
})
serve("obp" / "v1.1" prefix{
@@ -1141,7 +1142,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def postMoreInfoResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def postMoreInfoResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[MoreInfoJSON]
} match {
@@ -1182,13 +1183,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- postMoreInfoResponce(bankId, accountId, viewId, otherAccountId, user)
+ postMoreInfoResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- postMoreInfoResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ postMoreInfoResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
@@ -1196,7 +1197,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def updateMoreInfoResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def updateMoreInfoResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[MoreInfoJSON]
} match {
@@ -1235,13 +1236,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- updateMoreInfoResponce(bankId, accountId, viewId, otherAccountId, user)
+ updateMoreInfoResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- updateMoreInfoResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ updateMoreInfoResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
@@ -1249,7 +1250,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def postURLResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def postURLResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[UrlJSON]
} match {
@@ -1290,13 +1291,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- postURLResponce(bankId, accountId, viewId, otherAccountId, user)
+ postURLResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- postURLResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ postURLResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
@@ -1304,7 +1305,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def updateURLResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def updateURLResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[UrlJSON]
} match {
@@ -1343,13 +1344,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- updateURLResponce(bankId, accountId, viewId, otherAccountId, user)
+ updateURLResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- updateURLResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ updateURLResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
@@ -1357,7 +1358,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def postImageUrlResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def postImageUrlResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[ImageUrlJSON]
} match {
@@ -1398,13 +1399,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- postImageUrlResponce(bankId, accountId, viewId, otherAccountId, user)
+ postImageUrlResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- postImageUrlResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ postImageUrlResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
@@ -1412,7 +1413,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def updateImageUrlResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def updateImageUrlResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[ImageUrlJSON]
} match {
@@ -1451,13 +1452,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- updateImageUrlResponce(bankId, accountId, viewId, otherAccountId, user)
+ updateImageUrlResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- updateImageUrlResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ updateImageUrlResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
@@ -1465,7 +1466,7 @@ object OBPAPI1_1 extends RestHelper with Loggable {
//log the API call
logAPICall
- def postOpenCorporatesUrlResponce(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
+ def postOpenCorporatesUrlResponse(bankId : BankId, accountId : AccountId, viewId : ViewId, otherAccountId: String, user : Box[User]) : JsonResponse =
tryo{
json.extract[OpenCorporatesUrlJSON]
} match {
@@ -1506,13 +1507,13 @@ object OBPAPI1_1 extends RestHelper with Loggable {
if(httpCode == 200)
{
val user = getUser(httpCode, oAuthParameters.get("oauth_token"))
- postOpenCorporatesUrlResponce(bankId, accountId, viewId, otherAccountId, user)
+ postOpenCorporatesUrlResponse(bankId, accountId, viewId, otherAccountId, user)
}
else
JsonResponse(ErrorMessage(message), Nil, Nil, httpCode)
}
else
- postOpenCorporatesUrlResponce(bankId, accountId, viewId, otherAccountId, Empty)
+ postOpenCorporatesUrlResponse(bankId, accountId, viewId, otherAccountId, Empty)
}
})
serve("obp" / "v1.1" prefix{
diff --git a/src/main/scala/code/api/OBPRestHelper.scala b/src/main/scala/code/api/OBPRestHelper.scala
index 8ba1b2448..170c81c7a 100644
--- a/src/main/scala/code/api/OBPRestHelper.scala
+++ b/src/main/scala/code/api/OBPRestHelper.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
@@ -28,7 +28,7 @@ Berlin 13359, Germany
Everett Sochowski : everett AT tesobe DOT com
Ayoub Benali: ayoub AT tesobe DOT com
- */
+ */
package code.api
@@ -43,6 +43,8 @@ import code.model.User
import code.api.OAuthHandshake._
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.Extraction
+import net.liftweb.util.Props
+import code.api.Constant._
trait APIFailure{
val msg : String
@@ -61,7 +63,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
@@ -79,24 +81,56 @@ trait OBPRestHelper extends RestHelper with Loggable {
val VERSION : String
def vPlusVersion = "v" + VERSION
- def apiPrefix = "obp" / vPlusVersion oPrefix _
- implicit def jsonResponseBoxToJsonReponse(box: Box[JsonResponse]): JsonResponse = {
+ def apiPrefix = (ApiPathZero / vPlusVersion).oPrefix(_)
+
+ /*
+ 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("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("API Failure: " + msg)
+ logger.info("jsonResponseBoxToJsonResponse case Failure API Failure: " + msg)
errorJsonResponse(msg)
}
case _ => errorJsonResponse()
}
}
- def failIfBadOauth(fn: (Box[User]) => Box[JsonResponse]) : JsonResponse = {
+ /*
+ 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("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)
+ }
+ case false => h(r)
+ }
+ }
+
+ def failIfBadAuthorizationHeader(fn: (Box[User]) => Box[JsonResponse]) : JsonResponse = {
if (isThereAnOAuthHeader) {
getUser match {
case Full(u) => fn(Full(u))
@@ -104,16 +138,26 @@ 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) && isThereDirectLoginHeader) {
+ DirectLogin.getUser match {
+ case Full(u) => fn(Full(u))
+ case _ => {
+ var (httpCode, message, directLoginParameters) = DirectLogin.validator("protectedResource", DirectLogin.getHttpMethod)
+ errorJsonResponse(message, httpCode)
+ }
+ }
+ } 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 =
@@ -129,22 +173,48 @@ 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?)
+
+ TODO: should this be moved to def serve() further down?
+ */
+
def oauthServe(handler : PartialFunction[Req, Box[User] => Box[JsonResponse]]) : Unit = {
val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = {
new PartialFunction[Req, () => Box[LiftResponse]] {
def apply(r : Req) = {
- failIfBadOauth {
- handler(r)
+ //check (in that order):
+ //if request is correct json
+ //if request matches PartialFunction cases for each defined url
+ //if request has correct oauth headers
+ failIfBadAuthorizationHeader {
+ 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() before
+ //the url cases don't match because json failed
+ r.json_? match {
+ case true =>
+ //Try to evaluate the json
+ 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/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs.scala
new file mode 100644
index 000000000..5fd207836
--- /dev/null
+++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs.scala
@@ -0,0 +1,23 @@
+package code.api.ResourceDocs1_4_0
+
+import code.api.OBPRestHelper
+
+
+import net.liftweb.common.Loggable
+
+
+object ResourceDocs extends OBPRestHelper with ResourceDocsAPIMethods with Loggable {
+
+ val VERSION = "1.4.0"
+
+
+ val routes = List(
+ ImplementationsResourceDocs.getResourceDocsObp,
+ ImplementationsResourceDocs.getResourceDocsSwagger
+ )
+
+ routes.foreach(route => {
+ oauthServe(apiPrefix{route})
+ })
+
+}
diff --git a/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
new file mode 100644
index 000000000..34e37bc4f
--- /dev/null
+++ b/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
@@ -0,0 +1,329 @@
+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._
+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 code.api.v2_1_0.{APIMethods210, OBPAPI2_1_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 APIMethods210 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.
+
+ logger.info(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion")
+
+ val resourceDocs = requestedApiVersion match {
+ case "2.1.0" => Implementations2_1_0.resourceDocs ++ Implementations2_0_0.resourceDocs ++ Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs
+ 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
+ }
+
+ logger.info(s"There are ${resourceDocs.length} resource docs available to $requestedApiVersion")
+
+ val versionRoutes = requestedApiVersion match {
+ case "2.1.0" => OBPAPI2_1_0.routes
+ 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")
+
+
+ // 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")
+
+ // Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that.
+ Some(activeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb)))
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ getResourceDocsObp,
+ 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 may contain html markup (generated from markdown on the server).
+ |
+ """,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagApiInfo)
+ )
+
+ // 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")}
+
+ // Todo add query parameters
+
+
+
+ // /api/item/search/foo or /api/item/search?q=foo
+// case "search" :: q JsonGet _ =>
+// (for {
+// searchString <- q ::: S.params("q")
+// item <- Item.search(searchString)
+// } yield item).distinct: JValue
+
+
+
+ def getResourceDocsObp : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "resource-docs" :: requestedApiVersion :: "obp" :: Nil JsonGet _ => {
+ user => {
+ for {
+ rd <- getResourceDocsList(cleanApiVersionString(requestedApiVersion))
+ } yield {
+ // Filter
+ val rdFiltered = filterResourceDocs(rd)
+ // Format the data as json
+ val json = JSONFactory1_4_0.createResourceDocsJson(rdFiltered)
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ getResourceDocsSwagger,
+ 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,
+ false,
+ false,
+ false,
+ List(apiTagApiInfo)
+ )
+
+
+
+
+
+/*
+Filter Resource Docs based on the query parameters, else return the full list.
+We don't assume a default catalog (as API Explorer does)
+so the caller must specify any required filtering by catalog explicitly.
+ */
+def filterResourceDocs(allResources: List[ResourceDoc]) : List[ResourceDoc] = {
+
+ def stringToOptBoolean (x: String) : Option[Boolean] = x.toLowerCase match {
+ case "true" | "yes" | "1" | "-1" => Some(true)
+ case "false" | "no" | "0" => Some(false)
+ case _ => Empty
+ }
+
+ val showCoreParam: Option[Boolean] = for {
+ x <- S.param("core")
+ y <- stringToOptBoolean(x)
+ } yield y
+
+ logger.info(s"showCore is $showCoreParam")
+
+ val showPSD2Param: Option[Boolean] = for {
+ x <- S.param("psd2")
+ y <- stringToOptBoolean(x)
+ } yield y
+
+ logger.info(s"showPSD2 is $showPSD2Param")
+
+ val showOBWGParam: Option[Boolean] = for {
+ x <- S.param("obwg")
+ y <- stringToOptBoolean(x)
+ } yield y
+
+ logger.info(s"showOBWG is $showOBWGParam")
+
+
+ val showCore = showCoreParam
+ val showOBWG = showOBWGParam
+ val showPSD2 = showPSD2Param
+
+ // Filter (include, exclude or ignore)
+ val filteredResources1 : List[ResourceDoc] = showCore match {
+ case Some(true) => allResources.filter(x => x.isCore == true)
+ case Some(false) => allResources.filter(x => x.isCore == false)
+ case _ => allResources
+ }
+
+ val filteredResources2 : List[ResourceDoc] = showPSD2 match {
+ case Some(true) => filteredResources1.filter(x => x.isPSD2 == true)
+ case Some(false) => filteredResources1.filter(x => x.isPSD2 == false)
+ case _ => filteredResources1
+ }
+
+ val filteredResources3 : List[ResourceDoc] = showOBWG match {
+ case Some(true) => filteredResources2.filter(x => x.isOBWG == true)
+ case Some(false) => filteredResources2.filter(x => x.isOBWG == false)
+ case _ => filteredResources2
+ }
+
+ filteredResources3
+}
+
+
+ def getResourceDocsSwagger : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "resource-docs" :: requestedApiVersion :: "swagger" :: Nil JsonGet _ => {
+ user => {
+ for {
+ rd <- getResourceDocsList(cleanApiVersionString(requestedApiVersion))
+ } yield {
+ // Filter
+ val rdFiltered = filterResourceDocs(rd)
+ // Format the data as json
+ val json = SwaggerJSONFactory.createSwaggerResourceDoc(rdFiltered, requestedApiVersion)
+ //Get definitions of objects of success responses
+ val jsonAST = SwaggerJSONFactory.loadDefinitions(rdFiltered)
+ // Merge both results and return
+ successJsonResponse(Extraction.decompose(json) merge jsonAST)
+ }
+ }
+ }
+ }
+
+
+
+
+
+
+ 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,
+ false,
+ false,
+ false,
+ List(apiTagApiInfo))
+ }
+
+
+
+ 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/ResourceDocs1_4_0/SwaggerJSONFactory.scala b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala
new file mode 100644
index 000000000..deaedc59f
--- /dev/null
+++ b/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerJSONFactory.scala
@@ -0,0 +1,207 @@
+package code.api.ResourceDocs1_4_0
+
+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._
+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 {
+
+ 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,
+ description: 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], requestedApiVersion: String): SwaggerResourceDoc = {
+
+ def getName(rd: ResourceDoc) = {
+ rd.apiFunction match {
+ case "allBanks" => Some(ResponseObjectSchemaJson("#/definitions/BanksJSON"))
+ case "bankById" => Some(ResponseObjectSchemaJson("#/definitions/BankJSON"))
+ case _ => None
+ }
+ }
+
+ implicit val formats = DefaultFormats
+
+ val pegDownProcessor : PegDownProcessor = new PegDownProcessor
+
+ val contact = ContactJson("TESOBE Ltd. / Open Bank Project", "https://openbankproject.com")
+ val apiVersion = requestedApiVersion
+ val title = "Open Bank Project API"
+ val description = "An Open Source API for Banks. (c) TESOBE Ltd. 2011 - 2016. Licensed under the AGPL and commercial licences."
+ val info = InfoJson(title, description, contact, apiVersion)
+ val host = Props.get("hostname", "unknown host").replaceFirst("http://", "").replaceFirst("https://", "")
+ val basePath = s"/$ApiPathZero/" + apiVersion
+ val schemas = List("http", "https")
+ 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.toLowerCase,
+ MethodJson(
+ List(s"${rd.apiVersion.toString}"),
+ rd.summary,
+ description = pegDownProcessor.markdownToHtml(rd.description.stripMargin).replaceAll("\n", ""),
+ s"${rd.apiVersion.toString}-${rd.apiFunction.toString}",
+ 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)
+
+ 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)
+ }
+
+
+ def translateEntity(entity: Any): String = {
+
+ //Get fields of runtime entities and put they into structure Map(nameOfField -> fieldAsObject)
+ val r = currentMirror.reflect(entity)
+ 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
+
+ //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"}"""
+ 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: 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 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"
+ }
+ }
+ //Exclude all unrecognised fields and make part of fields definition
+ val fields: String = properties filter (_.contains("unknown") == false) mkString (",")
+ //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 = {
+
+ implicit val formats = DefaultFormats
+
+ //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)
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/scala/code/api/bankmock.scala b/src/main/scala/code/api/bankmock.scala
index e5b63752c..5771ac922 100644
--- a/src/main/scala/code/api/bankmock.scala
+++ b/src/main/scala/code/api/bankmock.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/main/scala/code/api/constant/constant.scala b/src/main/scala/code/api/constant/constant.scala
new file mode 100644
index 000000000..9f5f3d63f
--- /dev/null
+++ b/src/main/scala/code/api/constant/constant.scala
@@ -0,0 +1,17 @@
+package code.api
+
+import code.api.util.ErrorMessages
+import net.liftweb.common.Loggable
+import net.liftweb.util.Props
+
+
+// Note: Import this with: import code.api.Constant._
+object Constant extends Loggable {
+ logger.info("Instantiating Constants")
+
+ final val HostName = Props.get("hostname").openOrThrowException(ErrorMessages.HostnameNotSpecified)
+
+ // This is the part before the version. Do not change this default!
+ final val ApiPathZero = Props.get("apiPathZero", "obp")
+
+}
diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala
new file mode 100644
index 000000000..5ae25231c
--- /dev/null
+++ b/src/main/scala/code/api/directlogin.scala
@@ -0,0 +1,319 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2016, 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 Ltd.
+Osloer Strasse 16/17
+Berlin 13359, Germany
+
+ This product includes software developed at
+ TESOBE (http://www.tesobe.com/)
+
+ */
+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, Props}
+import net.liftweb.util.Helpers._
+
+import scala.compat.Platform
+import code.api.util.{APIUtil, ErrorMessages}
+
+/**
+* This object provides the API calls necessary to
+* authenticate users using JSON Web Tokens (http://jwt.io).
+*/
+
+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 {
+
+ // Our version of 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 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)
+
+ if (httpCode == 200) {
+ val userId:Long = (for {id <- getUserId(directLoginParameters)} yield id).getOrElse(0)
+
+ if (userId == 0) {
+ message = ErrorMessages.InvalidLoginCredentials
+ httpCode = 401
+ } else {
+ val claims = Map("" -> "")
+ val (token:String, secret:String) = generateTokenAndSecret(claims)
+
+ //Save the token that we have generated
+ if (saveAuthorizationToken(directLoginParameters, token, secret, userId)) {
+ message = token
+ } else {
+ httpCode = 500
+ message = "invalid"
+ }
+ }
+ }
+
+ if (httpCode == 200)
+ successJsonResponse(Extraction.decompose(JSONFactory.createTokenJSON(message)))
+ else
+ errorJsonResponse(message, httpCode)
+ }
+ }
+
+ def getHttpMethod = S.request match {
+ case Full(s) => s.post_? match {
+ case true => "POST"
+ case _ => "GET"
+ }
+ 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
+ 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) = {
+ val directLoginPossibleParameters =
+ List(
+ "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)
+ Some(split(0), parameterValue) // return key , value
+ else
+ None
+ }
+ else
+ None
+ }
+ //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(header) => {
+ if (header.contains("DirectLogin"))
+ toMap(header)
+ else
+ Map("error" -> "header incorrect")
+ }
+ case _ => Map("error" -> "missing header")
+ }
+ case _ => Map("error" -> "request incorrect")
+ }
+ }
+
+ def validAccessToken(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 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
+
+ val parameters = getAllParameters
+
+ //are all the necessary directLogin parameters present?
+ val missingParams = missingDirectLoginParameters(parameters, requestType)
+ if (missingParams.nonEmpty) {
+ message = ErrorMessages.DirectLoginMissingParameters + missingParams.mkString(", ")
+ httpCode = 400
+ }
+ else if (
+ requestType == "protectedResource" &&
+ ! validAccessToken(parameters.getOrElse("token", ""))
+ ) {
+ message = ErrorMessages.DirectLoginInvalidToken + parameters.getOrElse("token", "")
+ httpCode = 401
+ }
+ //check if the application is registered and active
+ else if (
+ requestType == "authorizationToken" &&
+ Props.getBool("direct_login_consumer_key_mandatory", true) &&
+ ! APIUtil.registeredApplication(parameters.getOrElse("consumer_key", ""))) {
+
+ logger.error("application: " + parameters.getOrElse("consumer_key", "") + " not found")
+ message = ErrorMessages.InvalidConsumerKey
+ httpCode = 401
+ }
+ else
+ httpCode = 200
+ if(message.nonEmpty)
+ logger.error("error message : " + message)
+ (httpCode, message, parameters)
+ }
+
+ private def generateTokenAndSecret(claims: Map[String,String]) =
+ {
+ // 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)
+ }
+
+ private def saveAuthorizationToken(directLoginParameters: Map[String, String], tokenKey: String, tokenSecret: String, userId: Long) =
+ {
+ import code.model.{Token, TokenType}
+ val token = Token.create
+ token.tokenType(TokenType.Access)
+ Consumer.find(By(Consumer.key, directLoginParameters.getOrElse("consumer_key", ""))) match {
+ case Full(consumer) => token.consumerId(consumer.id)
+ case _ => None
+ }
+ token.userForeignKey(userId)
+ 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()
+ tokenSaved
+ }
+
+ def getUser : Box[User] = {
+ val httpMethod = S.request match {
+ case Full(r) => r.request.method
+ case _ => "GET"
+ }
+ val (httpCode, message, directLoginParameters) = validator("protectedResource", httpMethod)
+
+ val user = for {
+ u <- getUserFromToken(if (directLoginParameters.isDefinedAt("token")) directLoginParameters.get("token") else Empty)
+ } yield u
+
+ if (user.isEmpty )
+ ParamFailure(message, Empty, Empty, APIFailure(message, httpCode))
+ else
+ user
+ }
+
+
+ private def getUserId(directLoginParameters: Map[String, String]): Box[Long] = {
+ val username = directLoginParameters.getOrElse("username", "")
+ val password = directLoginParameters.getOrElse("password", "")
+
+ var userId = for {id <- OBPUser.getUserId(username, password)} yield id
+
+ if (userId.isEmpty) {
+ OBPUser.externalUserHelper(username, password)
+ userId = for {id <- OBPUser.getUserId(username, password)} yield id
+ }
+
+ userId
+ }
+
+
+ def getUserFromToken(tokenID : Box[String]) : Box[User] = {
+ 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.name + " was found from the DirectLogin token")
+ case _ => logger.info("no user was found for the DirectLogin token")
+ }
+ user
+ }
+ case _ => {
+ logger.warn("no token " + tokenID.getOrElse("") + " found")
+ Empty
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/scala/code/api/metrics.scala b/src/main/scala/code/api/metrics.scala
index e0a13a3c2..b373e23d9 100644
--- a/src/main/scala/code/api/metrics.scala
+++ b/src/main/scala/code/api/metrics.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/main/scala/code/api/oauth1.0.scala b/src/main/scala/code/api/oauth1.0.scala
index 42f86f677..cc64908f0 100644
--- a/src/main/scala/code/api/oauth1.0.scala
+++ b/src/main/scala/code/api/oauth1.0.scala
@@ -1,29 +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 .
-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.
+Email: contact@tesobe.com
+TESOBE Ltd.
+Osloer Strasse 16/17
+Berlin 13359, Germany
- 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
+This product includes software developed at
+TESOBE (http://www.tesobe.com/)
+
*/
package code.api
import net.liftweb.http.rest.RestHelper
@@ -31,17 +30,20 @@ import net.liftweb.http.Req
import net.liftweb.http.PostRequest
import net.liftweb.common.Box
import net.liftweb.http.InMemoryResponse
-import net.liftweb.common.{Full,Empty,Loggable}
+import net.liftweb.common.{Empty, Full, Loggable}
import net.liftweb.http.S
-import code.model.{Nonce, Consumer, Token}
+import code.model.{Consumer, Nonce, Token}
import net.liftweb.mapper.By
import java.util.Date
-import java.net.{URLEncoder, URLDecoder}
+import java.net.{URLDecoder, URLEncoder}
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
+
import net.liftweb.util.Helpers
+
import scala.compat.Platform
import Helpers._
+import code.api.util.APIUtil
import net.liftweb.util.Props
import code.model.TokenType
import code.model.User
@@ -57,7 +59,7 @@ object OAuthHandshake extends RestHelper with Loggable {
serve
{
//Handling get request for a "request token"
- case Req("oauth" :: "initiate" :: Nil,_ ,PostRequest) =>
+ case Req("oauth" :: "initiate" :: Nil,_ , PostRequest) =>
{
//Extract the OAuth parameters from the header and test if the request is valid
var (httpCode, message, oAuthParameters) = validator("requestToken", "POST")
@@ -102,7 +104,7 @@ object OAuthHandshake extends RestHelper with Loggable {
}
}
- //Check if the request (access toke or request token) is valid and return a tuple
+ //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 OAuth parameters : oauth_prameter -> value
def getAllParameters : Map[String,String]= {
@@ -172,7 +174,7 @@ object OAuthHandshake extends RestHelper with Loggable {
output
}
- def suportedOAuthVersion(OAuthVersion : Option[String]) : Boolean = {
+ def supportedOAuthVersion(OAuthVersion : Option[String]) : Boolean = {
//auth_version is OPTIONAL. If present, MUST be set to "1.0".
OAuthVersion match
{
@@ -180,6 +182,7 @@ object OAuthHandshake extends RestHelper with Loggable {
case _ => true
}
}
+
def wrongTimestamp(requestTimestamp : Option[String]) : Option[String] = {
requestTimestamp match {
case Some(timestamp) => {
@@ -209,8 +212,7 @@ object OAuthHandshake extends RestHelper with Loggable {
}
}
- def alReadyUsedNonce(parameters : Map[String, String]) : Boolean = {
-
+ def alreadyUsedNonce(parameters : Map[String, String]) : Boolean = {
/*
* The nonce value MUST be unique across all requests with the
* same timestamp, client credentials, and token combinations.
@@ -225,12 +227,6 @@ object OAuthHandshake extends RestHelper with Loggable {
) !=0
}
- def registeredApplication(consumerKey : String ) : Boolean = {
- Consumer.find(By(Consumer.key,consumerKey)) match {
- case Full(application) => application.isActive
- case _ => false
- }
- }
def correctSignature(OAuthparameters : Map[String, String], httpMethod : String) = {
//Normalize an encode the request parameters as explained in Section 3.4.1.3.2
//of OAuth 1.0 specification (http://tools.ietf.org/html/rfc5849)
@@ -249,8 +245,7 @@ object OAuthHandshake extends RestHelper with Loggable {
parameters
}
-
- //prepare the base string
+ //prepare the base string (should we really have openOr here?)
var baseString = httpMethod+"&"+URLEncoder.encode(Props.get("hostname").openOr(S.hostAndPath) + S.uri,"UTF-8")+"&"
baseString+= generateOAuthParametersString(OAuthparameters)
@@ -268,14 +263,14 @@ object OAuthHandshake extends RestHelper with Loggable {
}
case _ => secret+= "&"
}
- logger.info("base string : " + baseString)
+ logger.info("base string: " + baseString)
//signing process
- val signingAlgorithm : String = if(OAuthparameters.get("oauth_signature_method").get.toLowerCase == "hmac-sha256")
- "HmacSHA256"
- else
- "HmacSHA1"
+ val signingAlgorithm : String = if(OAuthparameters.get("oauth_signature_method").get.toLowerCase == "hmac-sha256")
+ "HmacSHA256"
+ else
+ "HmacSHA1"
- logger.info("signing method:" + signingAlgorithm)
+ logger.info("signing method: " + signingAlgorithm)
logger.info("signing key: " + secret)
logger.info("signing key in bytes: " + secret.getBytes("UTF-8"))
@@ -283,9 +278,9 @@ object OAuthHandshake extends RestHelper with Loggable {
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:" + OAuthparameters.get("oauth_signature").get)
- logger.info("received signature after decoding:" + URLDecoder.decode(OAuthparameters.get("oauth_signature").get))
+ logger.info("calculatedSignature: " + calculatedSignature)
+ //logger.info("received signature:" + OAuthparameters.get("oauth_signature").get)
+ logger.info("received signature after decoding: " + URLDecoder.decode(OAuthparameters.get("oauth_signature").get))
calculatedSignature== URLDecoder.decode(OAuthparameters.get("oauth_signature").get,"UTF-8")
}
@@ -307,7 +302,7 @@ object OAuthHandshake extends RestHelper with Loggable {
}
//@return the missing parameters depending of the request type
- def missingOauthParameters(parameters : Map[String, String], requestType : String) : Set[String] = {
+ def missingOAuthParameters(parameters : Map[String, String], requestType : String) : Set[String] = {
val parametersBase =
List(
"oauth_consumer_key",
@@ -332,13 +327,13 @@ object OAuthHandshake extends RestHelper with Loggable {
oauthSignatureMethod.toLowerCase == "hmac-sha1"
}
- var message =""
+ var message = ""
var httpCode : Int = 500
var parameters = getAllParameters
- //does all the OAuth parameters are presents?
- val missingParams = missingOauthParameters(parameters,requestType)
+ //are all the necessary OAuth parameters present?
+ val missingParams = missingOAuthParameters(parameters,requestType)
if( missingParams.size != 0 )
{
message = "the following parameters are missing : " + missingParams.mkString(", ")
@@ -351,19 +346,19 @@ object OAuthHandshake extends RestHelper with Loggable {
httpCode = 400
}
//valid OAuth
- else if(!suportedOAuthVersion(parameters.get("oauth_version")))
+ else if(!supportedOAuthVersion(parameters.get("oauth_version")))
{
message = "OAuth version not supported"
httpCode = 400
}
//supported signature method
- else if (! supportedSignatureMethod(parameters.get("oauth_signature_method").get))
+ else if (!supportedSignatureMethod(parameters.get("oauth_signature_method").get))
{
- message = "Unsupported signature method, please use hmac-sha128 or hmac-sha256"
+ 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("oauth_consumer_key").get))
+ else if(! APIUtil.registeredApplication(parameters.get("oauth_consumer_key").get))
{
logger.error("application: " + parameters.get("oauth_consumer_key").get + " not found")
message = "Invalid consumer credentials"
@@ -376,7 +371,7 @@ object OAuthHandshake extends RestHelper with Loggable {
httpCode = 400
}
//unused nonce
- else if (alReadyUsedNonce(parameters))
+ else if (alreadyUsedNonce(parameters))
{
message = "Nonce already used"
httpCode = 401
@@ -404,18 +399,21 @@ 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)
}
- 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 generateTokenAndSecret() =
+ {
+ // generate some random strings
+ val token_message = Helpers.randomString(40)
+ val secret_message = Helpers.randomString(40)
+
+ (token_message, secret_message)
+ }
+
private def saveRequestToken(oAuthParameters : Map[String, String], tokenKey : String, tokenSecret : String) =
{
import code.model.{Nonce, Token, TokenType}
@@ -447,6 +445,7 @@ object OAuthHandshake extends RestHelper with Loggable {
nonceSaved && tokenSaved
}
+
private def saveAuthorizationToken(oAuthParameters : Map[String, String], tokenKey : String, tokenSecret : String) =
{
import code.model.{Nonce, Token, TokenType}
@@ -480,6 +479,24 @@ object OAuthHandshake extends RestHelper with Loggable {
nonceSaved && tokenSaved
}
+ def getConsumer: List[Consumer] = {
+ val httpMethod = S.request match {
+ case Full(r) => r.request.method
+ case _ => "GET"
+ }
+ val (httpCode, message, oAuthParameters) = validator("protectedResource", httpMethod)
+ import code.model.Token
+ val consumer: Option[Consumer] = for {
+ tokenId: String <- oAuthParameters.get("oauth_token")
+ token: Token <- Token.find(By(Token.key, tokenId))
+ consumer: Consumer <- token.consumerId.foreign
+ } yield {
+ consumer
+ }
+ consumer.toList
+ }
+
+
def getUser : Box[User] = {
val httpMethod = S.request match {
case Full(r) => r.request.method
@@ -502,7 +519,7 @@ object OAuthHandshake extends RestHelper with Loggable {
val user = token.user
//just a log
user match {
- case Full(u) => logger.info("user " + u.emailAddress + " was found from the oauth token")
+ case Full(u) => logger.info("user " + u.name + " was found from the oauth token")
case _ => logger.info("no user was found for the oauth token")
}
user
diff --git a/src/main/scala/code/api/sandbox/SandboxApiCalls.scala b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala
new file mode 100644
index 000000000..6fa10dcdf
--- /dev/null
+++ b/src/main/scala/code/api/sandbox/SandboxApiCalls.scala
@@ -0,0 +1,41 @@
+package code.api.sandbox
+
+import code.api.{APIFailure, OBPRestHelper}
+import code.api.util.APIUtil._
+import code.sandbox.{OBPDataImport, SandboxDataImport}
+import code.util.Helper
+import net.liftweb.common.{Box, Full, Failure, Loggable}
+import net.liftweb.http.{JsonResponse, ForbiddenResponse, S}
+import net.liftweb.http.js.JE.JsRaw
+import net.liftweb.http.rest.RestHelper
+import net.liftweb.util.Helpers._
+import net.liftweb.util.Props
+
+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{
+
+ case "v1.0" :: "data-import" :: Nil JsonPost json -> _ => {
+ user =>
+ logger.info("Hello from v1.0 data-import")
+ for{
+ correctToken <- Props.get("sandbox_data_import_secret") ~> APIFailure("Data import is disabled for this API instance.", 403)
+ providedToken <- S.param("secret_token") ~> APIFailure("secret_token parameter required", 403)
+ tokensMatch <- Helper.booleanToBox(providedToken == correctToken) ~> APIFailure("incorrect secret token", 403)
+ importData <- tryo{json.extract[SandboxDataImport]} ?~ "invalid json"
+ importWorked <- OBPDataImport.importer.vend.importData(importData)
+ } yield {
+ successJsonResponse(JsRaw("{}"), 201)
+ }
+
+ }
+
+ })
+
+
+}
diff --git a/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json b/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json
new file mode 100644
index 000000000..47f7c7d67
--- /dev/null
+++ b/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json
@@ -0,0 +1,2350 @@
+{
+ "banks":[{
+ "id":"psd201-bank-x--uk",
+ "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":"psd201-bank-y--uk",
+ "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.xuk.x@example.com",
+ "password":"5232e7",
+ "display_name":"Robert XUk X"
+ },{
+ "email":"susan.xuk.x@example.com",
+ "password":"43ca4d",
+ "display_name":"Susan XUk X"
+ },{
+ "email":"anil.xuk.x@example.com",
+ "password":"d8c716",
+ "display_name":"Anil XUk X"
+ },{
+ "email":"ellie.xuk.x@example.com",
+ "password":"6187b9",
+ "display_name":"Ellie XUk X"
+ },{
+ "email":"robert.yuk.y@example.com",
+ "password":"e5046a",
+ "display_name":"Robert YUk Y"
+ },{
+ "email":"susan.yuk.y@example.com",
+ "password":"5b38a6",
+ "display_name":"Susan YUk Y"
+ },{
+ "email":"anil.yuk.y@example.com",
+ "password":"dcf03d",
+ "display_name":"Anil YUk Y"
+ },{
+ "email":"ellie.yuk.y@example.com",
+ "password":"4f9eaa",
+ "display_name":"Ellie YUk Y"
+ }],
+ "accounts":[{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk",
+ "label":"Robert XUk X - M35 ..699",
+ "number":"13759969699",
+ "type":"CURRENT PLUS",
+ "balance":{
+ "currency":"GBP",
+ "amount":"8084.32"
+ },
+ "IBAN":"BA12 1234 5123 4513 7599 6969 977",
+ "owners":["robert.xuk.x@example.com"],
+ "generate_public_view":false,
+ "generate_accountants_view":true,
+ "generate_auditors_view":true
+ },{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk",
+ "label":"Robert YUk Y - M35 ..699",
+ "number":"13759969699",
+ "type":"CURRENT PLUS",
+ "balance":{
+ "currency":"GBP",
+ "amount":"8084.32"
+ },
+ "IBAN":"BA12 1234 5123 4513 7599 6969 977",
+ "owners":["robert.yuk.y@example.com"],
+ "generate_public_view":false,
+ "generate_accountants_view":true,
+ "generate_auditors_view":true
+ }],
+ "transactions":[{
+ "id":"b52a3465-d484-4b9a-97da-308188af7c6a",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"British Gas"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Gas/Elec",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4519.29",
+ "value":"-114.55"
+ }
+ },{
+ "id":"d0a5824a-3e5d-4f9a-988c-a9a571a2b192",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Seven Trent Water"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Water",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4493.70",
+ "value":"-25.59"
+ }
+ },{
+ "id":"1f5bff73-b647-454a-8655-71ec0e9fdb07",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Duddingston Golf Club"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Monthly Golf membership",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4452.08",
+ "value":"-41.62"
+ }
+ },{
+ "id":"74831d1d-7a6a-4829-874c-991567caa410",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Council Tax"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Council Tax",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4426.49",
+ "value":"-25.59"
+ }
+ },{
+ "id":"238730ee-f3b2-4560-a33e-9b023defadd9",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-07-05T00:00:00.000Z",
+ "completed":"2015-07-05T00:00:00.000Z",
+ "new_balance":"4322.85",
+ "value":"-103.64"
+ }
+ },{
+ "id":"f2ef31ea-468f-460e-b5ad-743551229b9d",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-07-05T00:00:00.000Z",
+ "completed":"2015-07-05T00:00:00.000Z",
+ "new_balance":"4256.21",
+ "value":"-66.64"
+ }
+ },{
+ "id":"58aee5c0-e189-469f-9415-48fa6a2557cc",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-07-08T00:00:00.000Z",
+ "completed":"2015-07-08T00:00:00.000Z",
+ "new_balance":"4244.34",
+ "value":"-11.87"
+ }
+ },{
+ "id":"5332a71d-e6b7-455a-a1d9-cb2a9d2931fe",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Prezzo"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-07-10T00:00:00.000Z",
+ "completed":"2015-07-10T00:00:00.000Z",
+ "new_balance":"4206.92",
+ "value":"-37.42"
+ }
+ },{
+ "id":"d5aec1cf-a1b9-46a6-a527-0fdcaa82c0bd",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-07-12T00:00:00.000Z",
+ "completed":"2015-07-12T00:00:00.000Z",
+ "new_balance":"4162.07",
+ "value":"-44.85"
+ }
+ },{
+ "id":"6a80acac-7445-4067-bc8b-36dfe09daa74",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"The Florist"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Florist",
+ "posted":"2015-07-12T00:00:00.000Z",
+ "completed":"2015-07-12T00:00:00.000Z",
+ "new_balance":"4136.48",
+ "value":"-25.59"
+ }
+ },{
+ "id":"baa18931-0352-4d1f-9149-70fca01463d0",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-07-13T00:00:00.000Z",
+ "completed":"2015-07-13T00:00:00.000Z",
+ "new_balance":"4129.87",
+ "value":"-6.61"
+ }
+ },{
+ "id":"2bb20bfa-971d-4036-b72c-c6a403a734ec",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"NCP Car Park"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Parking",
+ "posted":"2015-07-13T00:00:00.000Z",
+ "completed":"2015-07-13T00:00:00.000Z",
+ "new_balance":"4124.90",
+ "value":"-4.97"
+ }
+ },{
+ "id":"248a8538-fbac-4963-8bb0-b0b1b2324bed",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"The Kitchin"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-07-18T00:00:00.000Z",
+ "completed":"2015-07-18T00:00:00.000Z",
+ "new_balance":"4037.40",
+ "value":"-87.50"
+ }
+ },{
+ "id":"2a1f9fd6-22ba-4e02-bb20-44e75af20138",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-07-20T00:00:00.000Z",
+ "completed":"2015-07-20T00:00:00.000Z",
+ "new_balance":"3970.21",
+ "value":"-67.19"
+ }
+ },{
+ "id":"9e4a5cb7-516f-43e3-826b-16cb317f8a2b",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-07-20T00:00:00.000Z",
+ "completed":"2015-07-20T00:00:00.000Z",
+ "new_balance":"3903.57",
+ "value":"-66.64"
+ }
+ },{
+ "id":"5eb689ac-d36e-4ea8-8660-da0703b8ab25",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Bar Bar"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Bar",
+ "posted":"2015-07-24T00:00:00.000Z",
+ "completed":"2015-07-24T00:00:00.000Z",
+ "new_balance":"3858.81",
+ "value":"-44.76"
+ }
+ },{
+ "id":"a5b08db9-8c13-4553-b6ef-6c92dfd651c4",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"British Telecom"
+ },
+ "details":{
+ "type":"10219",
+ "description":"BT",
+ "posted":"2015-07-29T00:00:00.000Z",
+ "completed":"2015-07-29T00:00:00.000Z",
+ "new_balance":"3822.36",
+ "value":"-36.45"
+ }
+ },{
+ "id":"d4676772-bbb3-4e9a-8f51-dfdfe3e03dc7",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Paid in"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Income",
+ "posted":"2015-07-29T00:00:00.000Z",
+ "completed":"2015-07-29T00:00:00.000Z",
+ "new_balance":"5632.68",
+ "value":"1810.32"
+ }
+ },{
+ "id":"5c8b2732-3b43-4bb1-a91f-bbcf9d6a39ca",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"RBS Savings"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Savings",
+ "posted":"2015-07-30T00:00:00.000Z",
+ "completed":"2015-07-30T00:00:00.000Z",
+ "new_balance":"5354.71",
+ "value":"-277.97"
+ }
+ },{
+ "id":"1de79f9f-af27-443a-8ba4-f39861fd1559",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"British Gas"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Gas/Elec",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5240.16",
+ "value":"-114.55"
+ }
+ },{
+ "id":"b5b88a10-4b6b-4ab3-bd42-8116200f9595",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Seven Trent Water"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Water",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5214.57",
+ "value":"-25.59"
+ }
+ },{
+ "id":"11cf722a-0d81-43de-9ee2-583dba52b950",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Duddingston Golf Club"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Monthly Golf membership",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5172.95",
+ "value":"-41.62"
+ }
+ },{
+ "id":"9474e86a-f01a-4695-8c77-bfd791b91987",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Council Tax"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Council Tax",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5147.36",
+ "value":"-25.59"
+ }
+ },{
+ "id":"a2853ed1-b175-461c-ae76-2d722efa92df",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"The News Shop"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-03T00:00:00.000Z",
+ "completed":"2015-08-03T00:00:00.000Z",
+ "new_balance":"5140.31",
+ "value":"-7.05"
+ }
+ },{
+ "id":"fefe2df6-4e0d-4775-966e-3d3941569a85",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-08-04T00:00:00.000Z",
+ "completed":"2015-08-04T00:00:00.000Z",
+ "new_balance":"5073.67",
+ "value":"-66.64"
+ }
+ },{
+ "id":"7cc94373-c6b1-44c1-b963-eeebf3a032c7",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"5063.97",
+ "value":"-9.70"
+ }
+ },{
+ "id":"15ac6d27-4601-4a1b-a58c-8d6b6781fbab",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Next"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"5022.19",
+ "value":"-41.78"
+ }
+ },{
+ "id":"4c9ea71a-e943-426f-8ed6-6479757bdb5b",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"House of Fraser"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"4941.09",
+ "value":"-81.10"
+ }
+ },{
+ "id":"8c11d3a4-09fd-441f-ae13-b5d64cc50c3a",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"La Favorita Restaurant"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"4831.99",
+ "value":"-109.10"
+ }
+ },{
+ "id":"78438733-8c84-4338-a941-d59513bd8e8e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-09T00:00:00.000Z",
+ "completed":"2015-08-09T00:00:00.000Z",
+ "new_balance":"4794.30",
+ "value":"-37.69"
+ }
+ },{
+ "id":"b46ccef7-6b07-4981-a8fc-1cec4b471f15",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Bar Bar"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Bar",
+ "posted":"2015-08-14T00:00:00.000Z",
+ "completed":"2015-08-14T00:00:00.000Z",
+ "new_balance":"4772.74",
+ "value":"-21.56"
+ }
+ },{
+ "id":"76cb8fcd-fc52-4da9-81cd-e7e20ba6beb5",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Prezzo"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-08-15T00:00:00.000Z",
+ "completed":"2015-08-15T00:00:00.000Z",
+ "new_balance":"4739.28",
+ "value":"-33.46"
+ }
+ },{
+ "id":"c53f8eb6-63e9-4dff-83ee-d644f354c297",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-08-19T00:00:00.000Z",
+ "completed":"2015-08-19T00:00:00.000Z",
+ "new_balance":"4734.05",
+ "value":"-5.23"
+ }
+ },{
+ "id":"aee18985-241e-4c0c-82d4-801480c44163",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-22T00:00:00.000Z",
+ "completed":"2015-08-22T00:00:00.000Z",
+ "new_balance":"4689.51",
+ "value":"-44.54"
+ }
+ },{
+ "id":"2c23e332-38d9-4a75-8ee6-addaa8368fd6",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-08-26T00:00:00.000Z",
+ "completed":"2015-08-26T00:00:00.000Z",
+ "new_balance":"4622.87",
+ "value":"-66.64"
+ }
+ },{
+ "id":"7cf28cab-eac6-4e33-bf85-832db8aa6415",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"British Telecom"
+ },
+ "details":{
+ "type":"10219",
+ "description":"BT",
+ "posted":"2015-08-29T00:00:00.000Z",
+ "completed":"2015-08-29T00:00:00.000Z",
+ "new_balance":"4586.42",
+ "value":"-36.45"
+ }
+ },{
+ "id":"258e022c-d031-4aac-81d4-a80d73fd9d90",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Paid in"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Income",
+ "posted":"2015-08-29T00:00:00.000Z",
+ "completed":"2015-08-29T00:00:00.000Z",
+ "new_balance":"6396.74",
+ "value":"1810.32"
+ }
+ },{
+ "id":"cd6ae178-7f21-49a5-863c-757745f87a47",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"RBS Savings"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Savings",
+ "posted":"2015-08-30T00:00:00.000Z",
+ "completed":"2015-08-30T00:00:00.000Z",
+ "new_balance":"6118.77",
+ "value":"-277.97"
+ }
+ },{
+ "id":"ccda1c15-0e39-4f89-a46b-9fc989546c7e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"British Gas"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Gas/Elec",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"6004.22",
+ "value":"-114.55"
+ }
+ },{
+ "id":"28113be3-5ba6-4852-b91e-8c1ba27121f2",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Seven Trent Water"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Water",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"5978.63",
+ "value":"-25.59"
+ }
+ },{
+ "id":"0c86dede-351a-46d8-88b4-03edefaf0ac7",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Duddingston Golf Club"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Monthly Golf membership",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"5937.01",
+ "value":"-41.62"
+ }
+ },{
+ "id":"a971a407-ac1e-4f40-bee2-8f4822205cc1",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Council Tax"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Council Tax",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"5911.42",
+ "value":"-25.59"
+ }
+ },{
+ "id":"b74d866c-688f-4e50-b4bf-7f429351c4d6",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-09T00:00:00.000Z",
+ "completed":"2015-09-09T00:00:00.000Z",
+ "new_balance":"5843.07",
+ "value":"-68.35"
+ }
+ },{
+ "id":"3bf72782-0809-4971-95e5-32afe6f99c20",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-09-09T00:00:00.000Z",
+ "completed":"2015-09-09T00:00:00.000Z",
+ "new_balance":"5762.61",
+ "value":"-80.46"
+ }
+ },{
+ "id":"4021822e-2148-4de2-b131-361aedb135b5",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"The Cellar Door"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-09-17T00:00:00.000Z",
+ "completed":"2015-09-17T00:00:00.000Z",
+ "new_balance":"5663.67",
+ "value":"-98.94"
+ }
+ },{
+ "id":"0349e10e-1d62-47f1-9e5c-baa5a651101a",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"5656.75",
+ "value":"-6.92"
+ }
+ },{
+ "id":"48366c4d-c269-490f-b4a0-402901ed475e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Next"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"5582.11",
+ "value":"-74.64"
+ }
+ },{
+ "id":"a2689d2d-02c5-41a0-95e3-61ba3d9f8548",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Next"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"5521.85",
+ "value":"-60.26"
+ }
+ },{
+ "id":"2643bc73-8283-4d57-9a00-bf482d7db040",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Pizza Express"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"5489.24",
+ "value":"-32.61"
+ }
+ },{
+ "id":"c5455117-8d52-4a74-a5ca-90c32468d832",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-22T00:00:00.000Z",
+ "completed":"2015-09-22T00:00:00.000Z",
+ "new_balance":"5414.65",
+ "value":"-74.59"
+ }
+ },{
+ "id":"6e4fa0ec-d026-4550-9711-bd781ed8e88c",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"British Telecom"
+ },
+ "details":{
+ "type":"10219",
+ "description":"BT",
+ "posted":"2015-09-29T00:00:00.000Z",
+ "completed":"2015-09-29T00:00:00.000Z",
+ "new_balance":"5378.20",
+ "value":"-36.45"
+ }
+ },{
+ "id":"31367f2a-90cf-4565-8935-d24e1be10449",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"Paid in"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Income",
+ "posted":"2015-09-29T00:00:00.000Z",
+ "completed":"2015-09-29T00:00:00.000Z",
+ "new_balance":"7188.52",
+ "value":"1810.32"
+ }
+ },{
+ "id":"c5c877b0-8e14-48d0-b503-8d18bd4318b1",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-x--uk"
+ },
+ "counterparty":{
+ "name":"RBS Savings"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Savings",
+ "posted":"2015-09-30T00:00:00.000Z",
+ "completed":"2015-09-30T00:00:00.000Z",
+ "new_balance":"6910.55",
+ "value":"-277.97"
+ }
+ },{
+ "id":"aa1009fc-e94b-4314-9904-bdf6746a8c0e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"British Gas"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Gas/Elec",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4547.48",
+ "value":"-86.36"
+ }
+ },{
+ "id":"92364056-8812-46ba-aaab-f42971e5072f",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Seven Trent Water"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Water",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4521.54",
+ "value":"-25.94"
+ }
+ },{
+ "id":"69fb7f16-34c7-4c28-b5d3-0627117eb058",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Duddingston Golf Club"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Monthly Golf membership",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4471.13",
+ "value":"-50.41"
+ }
+ },{
+ "id":"d2084b72-1a33-484b-adea-34d87c2baf6e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Council Tax"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Council Tax",
+ "posted":"2015-07-01T00:00:00.000Z",
+ "completed":"2015-07-01T00:00:00.000Z",
+ "new_balance":"4445.19",
+ "value":"-25.94"
+ }
+ },{
+ "id":"7b6a29e3-da2a-4a4f-b466-94f49d3d2ff0",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-07-05T00:00:00.000Z",
+ "completed":"2015-07-05T00:00:00.000Z",
+ "new_balance":"4337.10",
+ "value":"-108.09"
+ }
+ },{
+ "id":"87fb7c3a-d77d-4cee-a9c4-369607ef6742",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-07-05T00:00:00.000Z",
+ "completed":"2015-07-05T00:00:00.000Z",
+ "new_balance":"4283.24",
+ "value":"-53.86"
+ }
+ },{
+ "id":"4146261a-238d-4cc7-8341-53250337a418",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-07-08T00:00:00.000Z",
+ "completed":"2015-07-08T00:00:00.000Z",
+ "new_balance":"4269.11",
+ "value":"-14.13"
+ }
+ },{
+ "id":"c1fb0aa9-6fe6-4aad-b97c-a59b87f32e17",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Prezzo"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-07-10T00:00:00.000Z",
+ "completed":"2015-07-10T00:00:00.000Z",
+ "new_balance":"4224.49",
+ "value":"-44.62"
+ }
+ },{
+ "id":"ee12be8f-581d-414e-8019-f70ee62e9369",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-07-12T00:00:00.000Z",
+ "completed":"2015-07-12T00:00:00.000Z",
+ "new_balance":"4167.75",
+ "value":"-56.74"
+ }
+ },{
+ "id":"f984ad1f-34b7-4c18-a879-4e2704338ce0",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"The Florist"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Florist",
+ "posted":"2015-07-12T00:00:00.000Z",
+ "completed":"2015-07-12T00:00:00.000Z",
+ "new_balance":"4141.81",
+ "value":"-25.94"
+ }
+ },{
+ "id":"908c88c9-5720-410a-aacb-4641700f7dee",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-07-13T00:00:00.000Z",
+ "completed":"2015-07-13T00:00:00.000Z",
+ "new_balance":"4134.56",
+ "value":"-7.25"
+ }
+ },{
+ "id":"08788c27-ece1-4720-8e78-ecf8873a62f3",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"NCP Car Park"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Parking",
+ "posted":"2015-07-13T00:00:00.000Z",
+ "completed":"2015-07-13T00:00:00.000Z",
+ "new_balance":"4129.85",
+ "value":"-4.71"
+ }
+ },{
+ "id":"eef6fc80-f313-4848-a0a8-e9cbcf1b2619",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"The Kitchin"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-07-18T00:00:00.000Z",
+ "completed":"2015-07-18T00:00:00.000Z",
+ "new_balance":"4037.87",
+ "value":"-91.98"
+ }
+ },{
+ "id":"5f695c4f-4248-47b8-9766-a7780de0429b",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-07-20T00:00:00.000Z",
+ "completed":"2015-07-20T00:00:00.000Z",
+ "new_balance":"3960.05",
+ "value":"-77.82"
+ }
+ },{
+ "id":"e20567ad-3238-4552-93cc-886d03f84728",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-07-20T00:00:00.000Z",
+ "completed":"2015-07-20T00:00:00.000Z",
+ "new_balance":"3906.19",
+ "value":"-53.86"
+ }
+ },{
+ "id":"22efbab1-74eb-4165-bc4b-8593ca8baa0f",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Bar Bar"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Bar",
+ "posted":"2015-07-24T00:00:00.000Z",
+ "completed":"2015-07-24T00:00:00.000Z",
+ "new_balance":"3867.68",
+ "value":"-38.51"
+ }
+ },{
+ "id":"4fef5ba9-d512-49f7-8cd0-08a9c35d5e21",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"British Telecom"
+ },
+ "details":{
+ "type":"10219",
+ "description":"BT",
+ "posted":"2015-07-29T00:00:00.000Z",
+ "completed":"2015-07-29T00:00:00.000Z",
+ "new_balance":"3832.03",
+ "value":"-35.65"
+ }
+ },{
+ "id":"8f18aef3-9ab8-4ea3-b090-cee134603078",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Paid in"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Income",
+ "posted":"2015-07-29T00:00:00.000Z",
+ "completed":"2015-07-29T00:00:00.000Z",
+ "new_balance":"6020.96",
+ "value":"2188.93"
+ }
+ },{
+ "id":"a8445b68-6e64-4e75-935d-4fff0cce69b9",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"RBS Savings"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Savings",
+ "posted":"2015-07-30T00:00:00.000Z",
+ "completed":"2015-07-30T00:00:00.000Z",
+ "new_balance":"5724.48",
+ "value":"-296.48"
+ }
+ },{
+ "id":"57dbba15-7a32-48d8-aae1-badfe606cd47",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"British Gas"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Gas/Elec",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5638.12",
+ "value":"-86.36"
+ }
+ },{
+ "id":"e3dfe76f-7985-49cb-8063-2feed4cce9d8",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Seven Trent Water"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Water",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5612.18",
+ "value":"-25.94"
+ }
+ },{
+ "id":"a7ae9bd6-bc43-41c8-8aaa-30ef99341926",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Duddingston Golf Club"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Monthly Golf membership",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5561.77",
+ "value":"-50.41"
+ }
+ },{
+ "id":"bd35f6bf-6f13-411b-973a-23a1d96ddd05",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Council Tax"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Council Tax",
+ "posted":"2015-08-01T00:00:00.000Z",
+ "completed":"2015-08-01T00:00:00.000Z",
+ "new_balance":"5535.83",
+ "value":"-25.94"
+ }
+ },{
+ "id":"ef2a8437-e9ac-4d34-ad9a-52b2f4cb4742",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"The News Shop"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-03T00:00:00.000Z",
+ "completed":"2015-08-03T00:00:00.000Z",
+ "new_balance":"5530.60",
+ "value":"-5.23"
+ }
+ },{
+ "id":"ee55ab26-7332-4f9a-9647-f6b5d130ab0a",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-08-04T00:00:00.000Z",
+ "completed":"2015-08-04T00:00:00.000Z",
+ "new_balance":"5476.74",
+ "value":"-53.86"
+ }
+ },{
+ "id":"f8b16c6e-b1cc-44f4-b43a-2f70a6359c63",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"5461.13",
+ "value":"-15.61"
+ }
+ },{
+ "id":"0d6debd3-6218-4820-8155-b9a80cd6571a",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Next"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"5430.21",
+ "value":"-30.92"
+ }
+ },{
+ "id":"5be03c5a-ed63-4c4c-aa7c-1c3225ffdeaa",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"House of Fraser"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"5351.21",
+ "value":"-79.00"
+ }
+ },{
+ "id":"a1e75e41-77e2-4e16-a825-b3d49e35720e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"La Favorita Restaurant"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-08-08T00:00:00.000Z",
+ "completed":"2015-08-08T00:00:00.000Z",
+ "new_balance":"5261.09",
+ "value":"-90.12"
+ }
+ },{
+ "id":"47bf901b-8733-4de5-bf64-873408da4ec5",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-09T00:00:00.000Z",
+ "completed":"2015-08-09T00:00:00.000Z",
+ "new_balance":"5225.66",
+ "value":"-35.43"
+ }
+ },{
+ "id":"0dad2ffe-74aa-434b-b960-ec52ecf04ba9",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Bar Bar"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Bar",
+ "posted":"2015-08-14T00:00:00.000Z",
+ "completed":"2015-08-14T00:00:00.000Z",
+ "new_balance":"5201.21",
+ "value":"-24.45"
+ }
+ },{
+ "id":"695807ff-697b-4b77-b324-99f0542ed622",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Prezzo"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-08-15T00:00:00.000Z",
+ "completed":"2015-08-15T00:00:00.000Z",
+ "new_balance":"5165.06",
+ "value":"-36.15"
+ }
+ },{
+ "id":"4aee735d-8d50-4674-9e8c-ae18ac8e8330",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-08-19T00:00:00.000Z",
+ "completed":"2015-08-19T00:00:00.000Z",
+ "new_balance":"5158.76",
+ "value":"-6.30"
+ }
+ },{
+ "id":"1edc54e8-f046-44d3-af62-6193e550a188",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-08-22T00:00:00.000Z",
+ "completed":"2015-08-22T00:00:00.000Z",
+ "new_balance":"5110.64",
+ "value":"-48.12"
+ }
+ },{
+ "id":"493b1d77-307e-4848-ba51-9704b3b47cd9",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-08-26T00:00:00.000Z",
+ "completed":"2015-08-26T00:00:00.000Z",
+ "new_balance":"5056.78",
+ "value":"-53.86"
+ }
+ },{
+ "id":"187aac2b-45ba-4416-ad1a-b0e31773b153",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"British Telecom"
+ },
+ "details":{
+ "type":"10219",
+ "description":"BT",
+ "posted":"2015-08-29T00:00:00.000Z",
+ "completed":"2015-08-29T00:00:00.000Z",
+ "new_balance":"5021.13",
+ "value":"-35.65"
+ }
+ },{
+ "id":"e2af6e62-02e3-42b6-b6e9-b4b4e0af31c9",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Paid in"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Income",
+ "posted":"2015-08-29T00:00:00.000Z",
+ "completed":"2015-08-29T00:00:00.000Z",
+ "new_balance":"7210.06",
+ "value":"2188.93"
+ }
+ },{
+ "id":"587316e9-d5cb-4ed9-8cb9-1a8d88bf3e7d",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"RBS Savings"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Savings",
+ "posted":"2015-08-30T00:00:00.000Z",
+ "completed":"2015-08-30T00:00:00.000Z",
+ "new_balance":"6913.58",
+ "value":"-296.48"
+ }
+ },{
+ "id":"6c36595a-53ea-4121-b174-768971b81b1a",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"British Gas"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Gas/Elec",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"6827.22",
+ "value":"-86.36"
+ }
+ },{
+ "id":"fce9bfba-4b00-4b50-97bf-85832b084d67",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Seven Trent Water"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Water",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"6801.28",
+ "value":"-25.94"
+ }
+ },{
+ "id":"d2a84876-1e22-41a3-9d7b-e08663dff65d",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Duddingston Golf Club"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Monthly Golf membership",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"6750.87",
+ "value":"-50.41"
+ }
+ },{
+ "id":"ad3abebe-9ba9-4b33-a050-8961028eb630",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Council Tax"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Council Tax",
+ "posted":"2015-09-01T00:00:00.000Z",
+ "completed":"2015-09-01T00:00:00.000Z",
+ "new_balance":"6724.93",
+ "value":"-25.94"
+ }
+ },{
+ "id":"81246b0e-6426-4e08-99a5-6ed5bb22f12f",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-09T00:00:00.000Z",
+ "completed":"2015-09-09T00:00:00.000Z",
+ "new_balance":"6664.38",
+ "value":"-60.55"
+ }
+ },{
+ "id":"bef6545c-c9d8-4cd6-9026-f8af2d45181d",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Shell Filling Station"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Filling Station",
+ "posted":"2015-09-09T00:00:00.000Z",
+ "completed":"2015-09-09T00:00:00.000Z",
+ "new_balance":"6588.64",
+ "value":"-75.74"
+ }
+ },{
+ "id":"3129a879-18fa-4656-a569-b8683e3be037",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"The Cellar Door"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-09-17T00:00:00.000Z",
+ "completed":"2015-09-17T00:00:00.000Z",
+ "new_balance":"6512.68",
+ "value":"-75.96"
+ }
+ },{
+ "id":"647fef7e-2596-44e5-9509-a6e8ca47112f",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Starbucks"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Coffee",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"6507.19",
+ "value":"-5.49"
+ }
+ },{
+ "id":"e04ab57f-9396-428e-82ca-d37cf1f16224",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Next"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"6426.94",
+ "value":"-80.25"
+ }
+ },{
+ "id":"a7e79d10-43a0-4b32-ab80-2ab9a374a87e",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Next"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"6368.49",
+ "value":"-58.45"
+ }
+ },{
+ "id":"7a353f19-c4e6-4d03-b585-f9caf8b98ffc",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Pizza Express"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Restaurants",
+ "posted":"2015-09-18T00:00:00.000Z",
+ "completed":"2015-09-18T00:00:00.000Z",
+ "new_balance":"6326.88",
+ "value":"-41.61"
+ }
+ },{
+ "id":"3f3e463a-5b45-4c75-b053-6a1a3feaabef",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Marks and Spencers"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Shopping",
+ "posted":"2015-09-22T00:00:00.000Z",
+ "completed":"2015-09-22T00:00:00.000Z",
+ "new_balance":"6227.52",
+ "value":"-99.36"
+ }
+ },{
+ "id":"7cd0002a-922a-48c6-9877-a7b91c45e9ce",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"British Telecom"
+ },
+ "details":{
+ "type":"10219",
+ "description":"BT",
+ "posted":"2015-09-29T00:00:00.000Z",
+ "completed":"2015-09-29T00:00:00.000Z",
+ "new_balance":"6191.87",
+ "value":"-35.65"
+ }
+ },{
+ "id":"ef4d17a4-d28e-4415-8052-1e7a3b7a2cbb",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"Paid in"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Income",
+ "posted":"2015-09-29T00:00:00.000Z",
+ "completed":"2015-09-29T00:00:00.000Z",
+ "new_balance":"8380.80",
+ "value":"2188.93"
+ }
+ },{
+ "id":"72e97d84-073f-42df-af83-7730e650f2ef",
+ "this_account":{
+ "id":"05237266-b334-4704-a087-5b460a2ecf04",
+ "bank":"psd201-bank-y--uk"
+ },
+ "counterparty":{
+ "name":"RBS Savings"
+ },
+ "details":{
+ "type":"10219",
+ "description":"Savings",
+ "posted":"2015-09-30T00:00:00.000Z",
+ "completed":"2015-09-30T00:00:00.000Z",
+ "new_balance":"8084.32",
+ "value":"-296.48"
+ }
+ }],
+ "branches":[{
+ "id":"9cf8-1234",
+ "bank_id":"psd201-bank-x--uk",
+ "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":"copyright2016",
+ "name":"Copyright ENBDG 2016"
+ }
+ },
+ "lobby":{
+ "hours":"Monday: 09:00 - 16:30Tuesday: 09:00 - 16:30Wednesday: 09:30 - 16:30Thursday: 09:00 - 16:30Friday: 09:00 - 16:30Saturday: 09:00 - 12:30Sunday: Closed"
+ },
+ "drive_up":{
+ "hours":""
+ }
+ },{
+ "id":"5620-7658",
+ "bank_id":"psd201-bank-x--uk",
+ "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":"copyright2016",
+ "name":"Copyright ENBDG 2016"
+ }
+ },
+ "lobby":{
+ "hours":"Monday: 09:30 - 15:00Tuesday: 09:30 - 15:00Wednesday: 09:30 - 15:00Thursday: 10:00 - 15:00Friday: 09:30 - 15:00Saturday: ClosedSunday: Closed"
+ },
+ "drive_up":{
+ "hours":""
+ }
+ },{
+ "id":"721a-18484",
+ "bank_id":"psd201-bank-y--uk",
+ "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":"copyright2016",
+ "name":"Copyright ENBDG 2016"
+ }
+ },
+ "lobby":{
+ "hours":"Monday: 09:00 - 17:00Tuesday: 09:00 - 17:00Wednesday: 09:30 - 17:00Thursday: 09:00 - 17:00Friday: 09:00 - 17:00Saturday: 09:00 - 13:00Sunday: Closed"
+ },
+ "drive_up":{
+ "hours":""
+ }
+ }],
+ "atms":[{
+ "id":"24a2-24242",
+ "bank_id":"psd201-bank-x--uk",
+ "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":"copyrightENBDG2016",
+ "name":"Copyright 2016 ENBDG"
+ }
+ }
+ },{
+ "id":"ab92-234",
+ "bank_id":"psd201-bank-y--uk",
+ "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":"copyrightENBDG2016",
+ "name":"Copyright 2016 ENBDG"
+ }
+ }
+ },{
+ "id":"feef-78987",
+ "bank_id":"psd201-bank-y--uk",
+ "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":"copyrightENBDG2016",
+ "name":"Copyright 2016 ENBDG"
+ }
+ }
+ }],
+ "products":[{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"6056-M35",
+ "name":"OFFSET FLEXIBLE MORTGAGE",
+ "category":"Mortgage",
+ "family":"Mortgage",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"bcef-H44",
+ "name":"SUPER GOLD",
+ "category":"Account",
+ "family":"Service",
+ "super_family":"Service",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"b192-J87",
+ "name":"Premier Mastercard",
+ "category":"Credit Card",
+ "family":"Credit Card",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"9499-JF",
+ "name":"Loan Quote",
+ "category":"Loan",
+ "family":"Loan",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"a24a-M55",
+ "name":"Generic MTA/Current Account",
+ "category":"Current Accounts",
+ "family":"Service",
+ "super_family":"Service",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"30f0-KJH",
+ "name":"Generic Savings",
+ "category":"Savings",
+ "family":"Credit",
+ "super_family":"Credit",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"8ed0-FDFD",
+ "name":"Generic Overdraft Product",
+ "category":"Overdraft",
+ "family":"Loan",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"be7a-DFD",
+ "name":"Generic MTA Upgrades Product",
+ "category":"MTA Upgrades",
+ "family":"MTA Upgrades",
+ "super_family":"MTA Upgrades",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-x--uk",
+ "code":"0dae-DS",
+ "name":"Generic Credit Card Product",
+ "category":"Credit Card",
+ "family":"Credit Card",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"7f3d-FDS",
+ "name":"OFFSET FLEXIBLE MORTGAGE",
+ "category":"Mortgage",
+ "family":"Mortgage",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"0e5c-F45",
+ "name":"RESERVE ACCOUNT",
+ "category":"Account",
+ "family":"Service",
+ "super_family":"Service",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"3ded-FDF",
+ "name":"Red Mastercard",
+ "category":"Credit Card",
+ "family":"Credit Card",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"ef52-AA",
+ "name":"Loan Quote",
+ "category":"Loan",
+ "family":"Loan",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"3d0c-SD4",
+ "name":"Generic MTA/Current Account",
+ "category":"Current Accounts",
+ "family":"Account",
+ "super_family":"Service",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"23ba-FD2",
+ "name":"Generic Savings",
+ "category":"Savings",
+ "family":"Credit",
+ "super_family":"Credit",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"2bc8-DF4",
+ "name":"Generic Overdraft Product",
+ "category":"Overdraft",
+ "family":"Loan",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"601d-FD5",
+ "name":"Generic MTA Upgrades Product",
+ "category":"MTA Upgrades",
+ "family":"MTA Upgrades",
+ "super_family":"MTA Upgrades",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"e401-DFD",
+ "name":"Generic Credit Card Product",
+ "category":"Credit Cards",
+ "family":"Credit Card",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"ef2e-DF4",
+ "name":"Generic MTA Upgrades Product",
+ "category":"MTA Upgrades",
+ "family":"MTA Upgrades",
+ "super_family":"MTA Upgrades",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ },{
+ "bank_id":"psd201-bank-y--uk",
+ "code":"a1c5-FD1",
+ "name":"Generic Credit Card Product",
+ "category":"Credit Cards",
+ "family":"Credit Card",
+ "super_family":"Lending",
+ "more_info_url":"www.example.com",
+ "meta":{
+ "license":{
+ "id":"copyright2015",
+ "name":"Copyright"
+ }
+ }
+ }],
+ "crm_events":[{
+ "id":"07c1efe8-38c2-41f2-ad18-f01d103af00d",
+ "bank_id":"psd201-bank-x--uk",
+ "customer":{
+ "name":"Dennis XUk X",
+ "number":"bank-x281290865"
+ },
+ "category":"TEST CATEGORY",
+ "detail":"TEST DETAIL",
+ "channel":"Test Channel",
+ "actual_date":"2015-02-14T00:00:00.000Z"
+ },{
+ "id":"cf3ec997-32ec-40bf-abeb-51a000d43a2c",
+ "bank_id":"psd201-bank-y--uk",
+ "customer":{
+ "name":"Dennis YUk Y",
+ "number":"bank-y282214387"
+ },
+ "category":"TEST CATEGORY",
+ "detail":"TEST DETAIL",
+ "channel":"Test Channel",
+ "actual_date":"2015-02-14T00:00:00.000Z"
+ }]
+}
\ No newline at end of file
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:30Tuesday: 09:00 - 16:30Wednesday: 09:30 - 16:30Thursday: 09:00 - 16:30Friday: 09:00 - 16:30Saturday: 09:00 - 12:30Sunday: 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:00Tuesday: 09:30 - 15:00Wednesday: 09:30 - 15:00Thursday: 10:00 - 15:00Friday: 09:30 - 15:00Saturday: ClosedSunday: 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:00Tuesday: 09:00 - 17:00Wednesday: 09:30 - 17:00Thursday: 09:00 - 17:00Friday: 09:00 - 17:00Saturday: 09:00 - 13:00Sunday: 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"
+ }]
+ }
diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala
index 54e277c93..857ea5835 100644
--- a/src/main/scala/code/api/util/APIUtil.scala
+++ b/src/main/scala/code/api/util/APIUtil.scala
@@ -1,50 +1,141 @@
/**
-Open Bank Project - API
-Copyright (C) 2011, 2013, 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.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 net.liftweb.common.Full
-import net.liftweb.http.{JsonResponse, S}
+import code.model._
+import dispatch.url
+import net.liftweb.common.{Empty, _}
import net.liftweb.http.js.JE.JsRaw
import net.liftweb.http.js.JsExp
-import net.liftweb.json.Extraction
+import net.liftweb.http.{CurrentReq, JsonResponse, Req, S}
import net.liftweb.json.JsonAST.JValue
+import net.liftweb.json.{Extraction, parse}
+import net.liftweb.mapper.By
import net.liftweb.util.Helpers._
+import net.liftweb.util.{Helpers, Props, SecurityHelpers}
-import scala.collection.JavaConversions.asScalaSet
+import scala.collection.mutable.ArrayBuffer
+import scala.collection.JavaConverters._
-object APIUtil {
+
+object ErrorMessages {
+
+ // Infrastructure / config messages
+ val HostnameNotSpecified = "OBP-00001: Hostname not specified. Could not get hostname from Props. Please edit your props file. Here are some example settings: hostname=http://127.0.0.1:8080 or hostname=https://www.example.com"
+
+ // General messages
+ val InvalidJsonFormat = "OBP-10001: Incorrect json format."
+ val InvalidNumber = "OBP-10002: Invalid Number. Could not convert value to a number."
+
+ // 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."
+ 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."
+ 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."
+
+ 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"
+ 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."
+ 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"
+ 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)."
+
+
+ val ConnectorEmptyResponse = "OBP-30200: Connector cannot return the data we requested."
+ val InvalidGetBankAccountsConnectorResponse = "OBP-30201: Connector did not return the set of accounts we requested."
+ val InvalidGetBankAccountConnectorResponse = "OBP-30202: Connector did not return the account we requested."
+ val InvalidGetTransactionConnectorResponse = "OBP-30203: Connector did not return the transaction we requested."
+
+ val EntitlementIsBankRole = "OBP-30205: This entitlement is a Bank Role. Please set bank_id to a valid bank id."
+ val EntitlementIsSystemRole = "OBP-30206: This entitlement is a System Role. Please set bank_id to empty string."
+
+ val InvalidGetTransactionsConnectorResponse = "OBP-30204: Connector did not return the set of transactions we requested."
+
+
+
+
+ // 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."
+
+
+
+
+
+
+}
+
+
+
+
+object APIUtil extends Loggable {
implicit val formats = net.liftweb.json.DefaultFormats
implicit def errorToJson(error: ErrorMessage): JValue = Extraction.decompose(error)
@@ -56,6 +147,16 @@ object APIUtil {
case _ => "GET"
}
+ def isThereDirectLoginHeader : Boolean = {
+ S.request match {
+ case Full(a) => a.header("Authorization") match {
+ case Full(parameters) => parameters.contains("DirectLogin")
+ case _ => false
+ }
+ case _ => false
+ }
+ }
+
def isThereAnOAuthHeader : Boolean = {
S.request match {
case Full(a) => a.header("Authorization") match {
@@ -66,16 +167,63 @@ object APIUtil {
}
}
- def logAPICall =
- APIMetrics.apiMetrics.vend.saveMetric(S.uriAndQueryString.getOrElse(""), (now: TimeSpan))
-
- def gitCommit : String = {
- val commit = tryo{
- val properties = new java.util.Properties()
- properties.load(getClass().getClassLoader().getResourceAsStream("git.properties"))
- properties.getProperty("git.commit.id", "")
+ def registeredApplication(consumerKey: String): Boolean = {
+ println(Consumer.findAll())
+ Consumer.find(By(Consumer.key, consumerKey)) match {
+ case Full(application) => application.isActive
+ case _ => false
}
- commit getOrElse ""
+ }
+
+ 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
+ val u = user.orNull
+ val userId = if (u != null) u.userId else "null"
+ val userName = if (u != null) u.name else "null"
+ var appName = "null"
+ var developerEmail = "null"
+ for (c <- getConsumer) {
+ appName = c.name.get
+ developerEmail = c.developerEmail.get
+ }
+ APIMetrics.apiMetrics.vend.saveMetric(userId, S.uriAndQueryString.getOrElse(""), (now: TimeSpan), userName, appName, developerEmail)
+ }
+ }
+
+
+ /*
+ 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 = 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
}
def noContentJsonResponse : JsonResponse =
@@ -84,23 +232,42 @@ object APIUtil {
def successJsonResponse(json: JsExp, httpCode : Int = 200) : JsonResponse =
JsonResponse(json, headers, Nil, httpCode)
+ def createdJsonResponse(json: JsExp, httpCode : Int = 201) : JsonResponse =
+ JsonResponse(json, headers, Nil, httpCode)
+
+ def acceptedJsonResponse(json: JsExp, httpCode : Int = 202) : JsonResponse =
+ JsonResponse(json, headers, Nil, httpCode)
+
def errorJsonResponse(message : String = "error", httpCode : Int = 400) : JsonResponse =
JsonResponse(Extraction.decompose(ErrorMessage(message)), headers, Nil, httpCode)
- def oauthHeaderRequiredJsonResponce : JsonResponse =
+ def notImplementedJsonResponse(message : String = "Not Implemented", httpCode : Int = 501) : JsonResponse =
+ JsonResponse(Extraction.decompose(ErrorMessage(message)), headers, Nil, httpCode)
+
+
+ def oauthHeaderRequiredJsonResponse : JsonResponse =
JsonResponse(Extraction.decompose(ErrorMessage("Authentication via OAuth is required")), headers, Nil, 400)
/** Import this object's methods to add signing operators to dispatch.Request */
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
+
+ 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)
@@ -136,7 +303,7 @@ import scala.collection.Map
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)
}
@@ -167,7 +334,7 @@ import scala.collection.Map
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 */
@@ -186,44 +353,319 @@ import scala.collection.Map
/** 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 < reqData.query_params
rb
}
- addHeader(
- rb,
+ createRequest( ReqData(
+ oauth_url,
+ r.getMethod,
+ body,
+ body_encoding,
IMap("Authorization" -> ("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
+ ))
}
}
}
+
+ /*
+ Used to document API calls / resources.
+
+ TODO Can we extract apiVersion, apiFunction, requestVerb and requestUrl from partialFunction?
+
+ */
+
+ // Used to tag Resource Docs
+ case class ResourceDocTag(tag: String)
+
+ // Use the *singular* case. for both the variable name and string.
+ // e.g. "This call is Payment related"
+ val apiTagTransactionRequest = ResourceDocTag("TransactionRequest")
+ val apiTagApiInfo = ResourceDocTag("APIInfo")
+ val apiTagBank = ResourceDocTag("Bank")
+ val apiTagAccount = ResourceDocTag("Account")
+ val apiTagPublicData = ResourceDocTag("PublicData")
+ val apiTagPrivateData = ResourceDocTag("PrivateData")
+ val apiTagTransaction = ResourceDocTag("Transaction")
+ val apiTagMetaData = ResourceDocTag("MetaData")
+ val apiTagView = ResourceDocTag("View")
+ val apiTagEntitlement = ResourceDocTag("Entitlement")
+ val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
+ 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 apiTagPerson = ResourceDocTag("Person")
+
+
+ // Used to document the API calls
+ 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.
+ requestUrl: String, // The URL (not including /obp/vX.X). Starts with / No trailing slash. TODO Constrain the string?
+ summary: String, // A summary of the call (originally taken from code comment) SHOULD be under 120 chars to be inline with Swagger
+ 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
+ isCore: Boolean,
+ isPSD2: Boolean,
+ isOBWG: Boolean,
+ tags: List[ResourceDocTag]
+ )
+
+ // Define relations between API end points. Used to create _links in the JSON and maybe later for API Explorer browsing
+ case class ApiRelation(
+ fromPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
+ toPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
+ rel : String
+ )
+
+ // Populated from Resource Doc and ApiRelation
+ case class InternalApiLink(
+ fromPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
+ toPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
+ rel : String,
+ requestUrl: String
+ )
+
+ // Used to pass context of current API call to the function that generates links for related Api calls.
+ case class DataContext(
+ user : Box[User],
+ bankId : Option[BankId],
+ accountId: Option[AccountId],
+ viewId: Option[ViewId],
+ counterpartyId: Option[CounterpartyId],
+ transactionId: Option[TransactionId]
+)
+
+ case class CallerContext(
+ caller : PartialFunction[Req, Box[User] => Box[JsonResponse]]
+ )
+
+ case class CodeContext(
+ resourceDocsArrayBuffer : ArrayBuffer[ResourceDoc],
+ relationsArrayBuffer : ArrayBuffer[ApiRelation]
+ )
+
+
+
+ case class ApiLink(
+ rel: String,
+ href: String
+ )
+
+ case class LinksJSON(
+ _links: List[ApiLink]
+ )
+
+ case class ResultAndLinksJSON(
+ result : JValue,
+ _links: List[ApiLink]
+ )
+
+
+ def createResultAndLinksJSON(result : JValue, links : List[ApiLink] ) : ResultAndLinksJSON = {
+ new ResultAndLinksJSON(
+ result,
+ links
+ )
+ }
+
+
+
+
+
+/*
+Returns a string showed to the developer
+ */
+ def authenticationRequiredMessage(authRequired: Boolean) : String =
+ authRequired match {
+ case true => "Authentication is Mandatory"
+ case false => "Authentication is Optional"
+ }
+
+
+
+ def apiVersionWithV(apiVersion : String) : String = {
+ // TODO Define a list of supported versions (put in Constant) and constrain the input
+ // Append v and replace _ with .
+ s"v${apiVersion.replaceAll("_",".")}"
+ }
+
+ def fullBaseUrl : String = {
+ val crv = CurrentReq.value
+ val apiPathZeroFromRequest = crv.path.partPath(0)
+ if (apiPathZeroFromRequest != ApiPathZero) throw new Exception("Configured ApiPathZero is not the same as the actual.")
+
+ val path = s"$HostName/$ApiPathZero"
+ path
+ }
+
+
+// Modify URL replacing placeholders for Ids
+ def contextModifiedUrl(url: String, context: DataContext) = {
+
+ // Potentially replace BANK_ID
+ val url2: String = context.bankId match {
+ case Some(x) => url.replaceAll("BANK_ID", x.value)
+ case _ => url
+ }
+
+ val url3: String = context.accountId match {
+ // Take care *not* to change OTHER_ACCOUNT_ID HERE
+ case Some(x) => url2.replaceAll("/ACCOUNT_ID", s"/${x.value}").replaceAll("COUNTERPARTY_ID", x.value)
+ case _ => url2
+ }
+
+ val url4: String = context.viewId match {
+ case Some(x) => url3.replaceAll("VIEW_ID", {x.value})
+ case _ => url3
+ }
+
+ val url5: String = context.counterpartyId match {
+ // Change OTHER_ACCOUNT_ID or COUNTERPARTY_ID
+ case Some(x) => url4.replaceAll("OTHER_ACCOUNT_ID", x.value).replaceAll("COUNTERPARTY_ID", x.value)
+ case _ => url4
+ }
+
+ val url6: String = context.transactionId match {
+ case Some(x) => url5.replaceAll("TRANSACTION_ID", x.value)
+ case _ => url5
+ }
+
+ // Add host, port, prefix, version.
+
+ // not correct because call could be in other version
+ val fullUrl = s"$fullBaseUrl$url6"
+
+ fullUrl
+ }
+
+
+ def getApiLinkTemplates(callerContext: CallerContext,
+ codeContext: CodeContext
+ ) : List[InternalApiLink] = {
+
+
+
+ // Relations of the API version where the caller is defined.
+ val relations = codeContext.relationsArrayBuffer.toList
+
+ // Resource Docs
+ // Note: This doesn't allow linking to calls in earlier versions of the API
+ // TODO: Fix me
+ val resourceDocs = codeContext.resourceDocsArrayBuffer
+
+ val pf = callerContext.caller
+
+ val internalApiLinks: List[InternalApiLink] = for {
+ relation <- relations.filter(r => r.fromPF == pf)
+ toResourceDoc <- resourceDocs.find(rd => rd.partialFunction == relation.toPF)
+ }
+ yield new InternalApiLink(
+ pf,
+ toResourceDoc.partialFunction,
+ relation.rel,
+ // Add the vVersion to the documented url
+ s"/${apiVersionWithV(toResourceDoc.apiVersion)}${toResourceDoc.requestUrl}"
+ )
+ internalApiLinks
+ }
+
+
+
+ // This is not currently including "templated" attribute
+ def halLinkFragment (link: ApiLink) : String = {
+ "\"" + link.rel +"\": { \"href\": \"" +link.href + "\" }"
+ }
+
+
+ // Since HAL links can't be represented via a case class, (they have dynamic attributes rather than a list) we need to generate them here.
+ def buildHalLinks(links: List[ApiLink]): JValue = {
+
+ val halLinksString = links match {
+ case head :: tail => tail.foldLeft("{"){(r: String, c: ApiLink) => ( r + " " + halLinkFragment(c) + " ," ) } + halLinkFragment(head) + "}"
+ case Nil => "{}"
+ }
+ parse(halLinksString)
+ }
+
+
+ // Returns API links (a list of them) that have placeholders (e.g. BANK_ID) replaced by values (e.g. ulster-bank)
+ def getApiLinks(callerContext: CallerContext, codeContext: CodeContext, dataContext: DataContext) : List[ApiLink] = {
+ val templates = getApiLinkTemplates(callerContext, codeContext)
+ // Replace place holders in the urls like BANK_ID with the current value e.g. 'ulster-bank' and return as ApiLinks for external consumption
+ val links = templates.map(i => ApiLink(i.rel,
+ contextModifiedUrl(i.requestUrl, dataContext) )
+ )
+ links
+ }
+
+
+ // Returns links formatted at objects.
+ def getHalLinks(callerContext: CallerContext, codeContext: CodeContext, dataContext: DataContext) : JValue = {
+ val links = getApiLinks(callerContext, codeContext, dataContext)
+ getHalLinksFromApiLinks(links)
+ }
+
+
+
+ def getHalLinksFromApiLinks(links: List[ApiLink]) : JValue = {
+ val halLinksJson = buildHalLinks(links)
+ 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 = {
+ !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/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala
new file mode 100644
index 000000000..578fba21f
--- /dev/null
+++ b/src/main/scala/code/api/util/ApiRole.scala
@@ -0,0 +1,62 @@
+package code.api.util
+
+sealed trait ApiRole{
+ val requiresBankId: Boolean
+}
+
+object ApiRole {
+
+ case object CanSearchAllTransactions extends ApiRole{
+ val requiresBankId = false
+ }
+ case object CanSearchAllAccounts extends ApiRole{
+ val requiresBankId = false
+ }
+ case object CanQueryOtherUser extends ApiRole{
+ val requiresBankId = false
+ }
+ case object CanSearchWarehouse extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanSearchMetrics extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanCreateCustomer extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanCreateAccount extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanGetAnyUser extends ApiRole{
+ val requiresBankId = false
+ }
+ case object CanCreateAnyTransactionRequest extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanAddSocialMediaHandle extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanGetSocialMediaHandles extends ApiRole{
+ val requiresBankId = true
+ }
+ case object CanCreateSandbox extends ApiRole{
+ val requiresBankId = false
+ }
+
+ def valueOf(value: String): ApiRole = value match {
+ case "CanSearchAllTransactions" => CanSearchAllTransactions
+ case "CanSearchAllAccounts" => CanSearchAllAccounts
+ case "CanQueryOtherUser" => CanQueryOtherUser
+ case "CanSearchWarehouse" => CanSearchWarehouse
+ case "CanSearchMetrics" => CanSearchMetrics
+ case "CanCreateCustomer" => CanCreateCustomer
+ case "CanCreateAccount" => CanCreateAccount
+ case "CanGetAnyUser" => CanGetAnyUser
+ case "CanCreateAnyTransactionRequest" => CanCreateAnyTransactionRequest
+ case "CanAddSocialMediaHandle" => CanAddSocialMediaHandle
+ case "CanGetSocialMediaHandles" => CanGetSocialMediaHandles
+ case "CanCreateSandbox" => CanCreateSandbox
+ case _ => throw new IllegalArgumentException()
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/scala/code/api/v1_2/JSONFactory1.2.scala b/src/main/scala/code/api/v1_2/JSONFactory1.2.scala
index 7307ef5ff..6dd55b819 100644
--- a/src/main/scala/code/api/v1_2/JSONFactory1.2.scala
+++ b/src/main/scala/code/api/v1_2/JSONFactory1.2.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/main/scala/code/api/v1_2/OBPAPI1.2.scala b/src/main/scala/code/api/v1_2/OBPAPI1.2.scala
index 3b1060438..54aac70df 100644
--- a/src/main/scala/code/api/v1_2/OBPAPI1.2.scala
+++ b/src/main/scala/code/api/v1_2/OBPAPI1.2.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
@@ -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/1_2_1/APIMethods121.scala b/src/main/scala/code/api/v1_2_1/APIMethods121.scala
similarity index 53%
rename from src/main/scala/code/api/1_2_1/APIMethods121.scala
rename to src/main/scala/code/api/v1_2_1/APIMethods121.scala
index 0c7d5a656..58d1c6b24 100644
--- a/src/main/scala/code/api/1_2_1/APIMethods121.scala
+++ b/src/main/scala/code/api/v1_2_1/APIMethods121.scala
@@ -2,24 +2,38 @@ package code.api.v1_2_1
import code.api.util.APIUtil
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._
import net.liftweb.http.rest.RestHelper
import java.net.URL
-import net.liftweb.util.Props
-import net.liftweb.json.Extraction
+import net.liftweb.util.{True, Props}
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
-case class MakePaymentJson(bank_id : String, account_id : String, amount : String)
+import scala.collection.immutable.Nil
+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,
+ amount : String)
trait APIMethods121 {
//needs to be a RestHelper to get access to JsonGet, JsonPost, etc.
@@ -57,23 +71,72 @@ 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(){
- def root(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
- case 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))
- }
+ val resourceDocs = ArrayBuffer[ResourceDoc]()
+ val emptyObjectJson : JValue = Nil
+ val apiVersion : String = "1_2_1"
+
+ resourceDocs += ResourceDoc(
+ root(apiVersion),
+ apiVersion,
+ "root",
+ "GET",
+ "/root",
+ "The root of the API",
+ """Returns information about:
+ |
+ |* API version
+ |* Hosted by information
+ |* Git Commit""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ false,
+ true,
+ apiTagApiInfo :: Nil)
+
+ def root(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "root" :: Nil JsonGet json => user => Full(successJsonResponse(getApiInfoJSON(apiVersion), 200))
+ case Nil JsonGet json => user => Full(successJsonResponse(getApiInfoJSON(apiVersion), 200))
}
- lazy val allBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+
+ resourceDocs += ResourceDoc(
+ getBanks,
+ apiVersion,
+ "allBanks",
+ "GET",
+ "/banks",
+ "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
+ |* Logo URL
+ |* Website""",
+ emptyObjectJson,
+ decompose(BanksJSON(List(BankJSON("1", "EFG", "Eurobank", "None", "www.eurobank.rs")))),
+ emptyObjectJson :: Nil,
+ true,
+ false,
+ true,
+ apiTagBank :: Nil)
+
+ lazy val getBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get banks
case "banks" :: Nil JsonGet json => {
user =>
@@ -89,6 +152,29 @@ trait APIMethods121 {
}
}
+
+ resourceDocs += ResourceDoc(
+ bankById,
+ apiVersion,
+ "bankById",
+ "GET",
+ "/banks/BANK_ID",
+ "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
+ |* Website""",
+ emptyObjectJson,
+ decompose(BankJSON("1", "EFG", "Eurobank", "None", "www.eurobank.rs")),
+ emptyObjectJson :: Nil,
+ true,
+ false,
+ true,
+ apiTagBank :: Nil)
+
+
lazy val bankById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get bank by id
case "banks" :: BankId(bankId) :: Nil JsonGet json => {
@@ -102,6 +188,33 @@ trait APIMethods121 {
}
}
+
+ resourceDocs += ResourceDoc(
+ allAccountsAllBanks,
+ apiVersion,
+ "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.
+ |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.
+ |
+ |Note for those upgrading from v1.2:
+ |The v1.2 version of this call was buggy in that it did not include public accounts if an authenticated user made the call.
+ |If you need the previous behaviour, please use the API call for private accounts (..../accounts/private).
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ apiTagAccount :: Nil)
+
lazy val allAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get accounts for all banks (private + public)
case "accounts" :: Nil JsonGet json => {
@@ -110,6 +223,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ privateAccountsAllBanks,
+ apiVersion,
+ "privateAccountsAllBanks",
+ "GET",
+ "/accounts/private",
+ "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.
+ |
+ |Authentication via OAuth is required.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ apiTagAccount :: Nil)
+
lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get private accounts for all banks
case "accounts" :: "private" :: Nil JsonGet json => {
@@ -123,6 +255,23 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ publicAccountsAllBanks,
+ apiVersion,
+ "publicAccountsAllBanks",
+ "GET",
+ "/accounts/public",
+ "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,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ apiTagAccount :: Nil)
+
lazy val publicAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get public accounts for all banks
case "accounts" :: "public" :: Nil JsonGet json => {
@@ -132,6 +281,30 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ allAccountsAtOneBank,
+ apiVersion,
+ "allAccountsAtOneBank",
+ "GET",
+ "/banks/BANK_ID/accounts",
+ "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.
+ |
+ |If the user is not authenticated via OAuth, the list will contain only the accounts providing public views.
+ |
+ |Note for those upgrading from v1.2:
+ |The v1.2 version of this call was buggy in that it did not include public accounts if an authenticated user made the call.
+ |If you need the previous behaviour, please use the API call for private accounts (..../accounts/private)
+ """,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ apiTagAccount :: 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 => {
@@ -145,6 +318,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ privateAccountsAtOneBank,
+ apiVersion,
+ "privateAccountsAtOneBank",
+ "GET",
+ "/banks/BANK_ID/accounts/private",
+ "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.
+ |
+ |Authentication via OAuth is required.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ apiTagAccount :: 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 => {
@@ -159,6 +351,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ publicAccountsAtOneBank,
+ apiVersion,
+ "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.
+ |
+ |Authentication via OAuth is not required.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ apiTagAccount :: apiTagPublicData :: 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 => {
@@ -172,6 +382,35 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ accountById,
+ apiVersion,
+ "accountById",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account",
+ "Get account by id.",
+ s"""Information returned about an account specified by ACCOUNT_ID as moderated by the view (VIEW_ID):
+ |
+ |* Number
+ |* Owners
+ |* Type
+ |* Balance
+ |* IBAN
+ |* Available views
+ |
+ |More details about the data moderation by the view [here](#1_2_1-getViewsForBankAccount).
+ |
+ |${authenticationRequiredMessage(false)}
+ |
+ |Authentication is required if the 'is_public' field in view (VIEW_ID) is not set to `true`.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ true,
+ false,
+ apiTagAccount :: Nil)
+
lazy val accountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get account by id
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "account" :: Nil JsonGet json => {
@@ -189,6 +428,77 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateAccountLabel,
+ apiVersion,
+ "updateAccountLabel",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagMetaData))
+
+ lazy val updateAccountLabel : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //change account label
+ // TODO Use PATCH instead? Remove BANK_ID AND ACCOUNT_ID from the body? (duplicated in URL)
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonPost json -> _ => {
+ user =>
+ for {
+ u <- user ?~ "user not found"
+ json <- tryo { json.extract[UpdateAccountJSON] } ?~ "wrong JSON format"
+ account <- BankAccount(bankId, accountId)
+ } yield {
+ account.updateLabel(u, json.label)
+ successJsonResponse(Extraction.decompose(SuccessMessage("ok")), 200)
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getViewsForBankAccount,
+ apiVersion,
+ "getViewsForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/views",
+ "Get Views for Account.",
+ """#Views
+ |
+ |
+ |Views in Open Bank Project provide a mechanism for fine grained access control and delegation to Accounts and Transactions. Account holders use the 'owner' view by default. Delegated access is made through other views for example 'accountants', 'share-holders' or 'tagging-application'. Views can be created via the API and each view has a list of entitlements.
+ |
+ |Views on accounts and transactions filter the underlying data to redact certain fields for certain users. For instance the balance on an account may be hidden from the public. The way to know what is possible on a view is determined in the following JSON.
+ |
+ |**Data:** When a view moderates a set of data, some fields my contain the value `null` rather than the original value. This indicates either that the user is not allowed to see the original data or the field is empty.
+ |
+ |There is currently one exception to this rule; the 'holder' field in the JSON contains always a value which is either an alias or the real name - indicated by the 'is_alias' field.
+ |
+ |**Action:** When a user performs an action like trying to post a comment (with POST API call), if he is not allowed, the body response will contain an error message.
+ |
+ |**Metadata:**
+ |Transaction metadata (like images, tags, comments, etc.) will appears *ONLY* on the view where they have been created e.g. comments posted to the public view only appear on the public view.
+ |
+ |The other account metadata fields (like image_URL, more_info, etc.) are unique through all the views. Example, if a user edits the 'more_info' field in the 'team' view, then the view 'authorities' will show the new value (if it is allowed to do it).
+ |
+ |# All
+ |*Optional*
+ |
+ |Returns the list of the views created for account ACCOUNT_ID at BANK_ID.
+ |
+ |OAuth authentication is required and the user needs to have access to the owner view.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagView))
+
lazy val getViewsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get the available views on an bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: Nil JsonGet json => {
@@ -196,7 +506,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))
@@ -204,13 +514,41 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ createViewForBankAccount,
+ apiVersion,
+ "createViewForBankAccount",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/views",
+ "Create View.",
+ """#Create a view on bank account
+ |
+ | OAuth authentication is required and the user needs to have access to the owner view.
+ | The 'alias' field in the JSON can take one of three values:
+ |
+ | * _public_: to use the public alias if there is one specified for the other account.
+ | * _private_: to use the public alias if there is one specified for the other account.
+ |
+ | * _''(empty string)_: to use no alias; the view shows the real name of the other account.
+ |
+ | The 'hide_metadata_if_alias_used' field in the JSON can take boolean values. If it is set to `true` and there is an alias on the other account then the other accounts' metadata (like more_info, url, image_url, open_corporates_url, etc.) will be hidden. Otherwise the metadata will be shown.
+ |
+ | The '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(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,
+ false,
+ false,
+ List(apiTagAccount, apiTagView))
+
lazy val createViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//creates a view on an bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: Nil JsonPost json -> _ => {
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 {
@@ -220,6 +558,27 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateViewForBankAccount,
+ apiVersion,
+ "updateViewForBankAccount",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID",
+ "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.
+ |
+ |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(UpdateViewJSON("New description of view", false, "_public_", true, List("can_see_transaction_start_date", "can_see_bank_account_label"))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagView))
+
lazy val updateViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//updates a view on a bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: ViewId(viewId) :: Nil JsonPut json -> _ => {
@@ -227,7 +586,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)
@@ -236,6 +595,22 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteViewForBankAccount,
+ apiVersion,
+ "deleteViewForBankAccount",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagView))
+
lazy val deleteViewForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//deletes a view on an bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "views" :: ViewId(viewId) :: Nil JsonDelete json => {
@@ -248,6 +623,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getPermissionsForBankAccount,
+ apiVersion,
+ "getPermissionsForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions",
+ "Get access.",
+ """Returns the list of the permissions at BANK_ID for account ACCOUNT_ID, with each time a pair composed of the user and the views that he has access to.
+ |
+ |OAuth authentication is required and the user needs to have access to the owner view.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagView, apiTagEntitlement)
+ )
+
lazy val getPermissionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get access
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: Nil JsonGet json => {
@@ -263,6 +657,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getPermissionForUserForBankAccount,
+ apiVersion,
+ "getPermissionForUserForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID",
+ "Get access for specific user.",
+ """Returns the list of the views at BANK_ID for account ACCOUNT_ID that a USER_ID at their provider PROVIDER_ID has access to.
+ |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.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagView, apiTagEntitlement))
+
lazy val getPermissionForUserForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get access for specific user
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: providerId :: userId :: Nil JsonGet json => {
@@ -278,6 +691,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addPermissionForUserForBankAccountForMultipleViews,
+ apiVersion,
+ "addPermissionForUserForBankAccountForMultipleViews",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/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.
+ |
+ |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,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired))
+
lazy val addPermissionForUserForBankAccountForMultipleViews : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add access for specific user to a list of views
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: providerId :: userId :: "views" :: Nil JsonPost json -> _ => {
@@ -294,6 +727,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addPermissionForUserForBankAccountForOneView,
+ apiVersion,
+ "addPermissionForUserForBankAccountForOneView",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/views/VIEW_ID",
+ "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.
+ |
+ |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,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired))
+
lazy val addPermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add access for specific user to a specific view
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: providerId :: userId :: "views" :: ViewId(viewId) :: Nil JsonPost json -> _ => {
@@ -301,6 +754,7 @@ trait APIMethods121 {
for {
u <- user ?~ "user not found"
account <- BankAccount(bankId, accountId)
+ // TODO Check Error cases
addedView <- account addPermission(u, ViewUID(viewId, bankId, accountId), providerId, userId)
} yield {
val viewJson = JSONFactory.createViewJSON(addedView)
@@ -309,6 +763,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ removePermissionForUserForBankAccountForOneView,
+ apiVersion,
+ "removePermissionForUserForBankAccountForOneView",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/views/VIEW_ID",
+ "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.
+ |
+ |OAuth authentication is required and the user needs to have access to the owner view.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired))
+
lazy val removePermissionForUserForBankAccountForOneView : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete access for specific user to one view
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: providerId :: userId :: "views" :: ViewId(viewId) :: Nil JsonDelete json => {
@@ -322,6 +796,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ removePermissionForUserForBankAccountForAllViews,
+ apiVersion,
+ "removePermissionForUserForBankAccountForAllViews",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID/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.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement, apiTagOwnerRequired))
+
lazy val removePermissionForUserForBankAccountForAllViews : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete access for specific user to all the views
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: providerId :: userId :: "views" :: Nil JsonDelete json => {
@@ -335,6 +827,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCounterpartiesForBankAccount,
+ apiVersion,
+ "getCounterpartiesForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts",
+ "Get counterparties of one account.",
+ 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,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagCounterparty))
+
lazy val getCounterpartiesForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get other accounts for one account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts" :: Nil JsonGet json => {
@@ -350,6 +860,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCounterpartyByIdForBankAccount,
+ apiVersion,
+ "getCounterpartyByIdForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID",
+ "Get counterparty by id.",
+ 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,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagCounterparty))
+
lazy val getCounterpartyByIdForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get one other account by id
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: Nil JsonGet json => {
@@ -365,6 +893,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCounterpartyMetadata,
+ apiVersion,
+ "getCounterpartyMetadata",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata",
+ "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,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val getCounterpartyMetadata : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get metadata of one other account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: Nil JsonGet json => {
@@ -381,6 +928,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCounterpartyPublicAlias,
+ apiVersion,
+ "getCounterpartyPublicAlias",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias",
+ "Get public alias of other bank account.",
+ 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,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val getCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get public alias of other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "public_alias" :: Nil JsonGet json => {
@@ -398,6 +963,30 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addCounterpartyPublicAlias,
+ apiVersion,
+ "addCounterpartyPublicAlias",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias",
+ "Add public alias to other bank account.",
+ s"""Creates the public alias for the other account OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.
+ |
+ |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.""",
+ Extraction.decompose(AliasJSON("An Alias")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add public alias to other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "public_alias" :: Nil JsonPost json -> _ => {
@@ -416,6 +1005,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyPublicAlias,
+ apiVersion,
+ "updateCounterpartyPublicAlias",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias",
+ "Update public alias of other bank account.",
+ s"""Updates the public alias of the other account / counterparty OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.""",
+ Extraction.decompose(AliasJSON("An Alias")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update public alias of other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "public_alias" :: Nil JsonPut json -> _ => {
@@ -434,6 +1042,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyPublicAlias,
+ apiVersion,
+ "deleteCounterpartyPublicAlias",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/public_alias",
+ "Delete Counterparty Public Alias",
+ s"""Deletes the public alias of the other account OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyPublicAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete public alias of other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "public_alias" :: Nil JsonDelete _ => {
@@ -449,6 +1076,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCounterpartyPrivateAlias,
+ apiVersion,
+ "getCounterpartyPrivateAlias",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias",
+ "Get Counterparty Private Alias",
+ s"""Returns the private alias of the other account OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val getCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get private alias of other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "private_alias" :: Nil JsonGet json => {
@@ -466,6 +1112,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addCounterpartyPrivateAlias,
+ apiVersion,
+ "addCounterpartyPrivateAlias",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias",
+ "Create Counterparty Private Alias",
+ s"""Creates a private alias for the other account OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.""",
+ Extraction.decompose(AliasJSON("An Alias")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add private alias to other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "private_alias" :: Nil JsonPost json -> _ => {
@@ -485,6 +1150,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyPrivateAlias,
+ apiVersion,
+ "updateCounterpartyPrivateAlias",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias",
+ "Update Counterparty Private Alias",
+ s"""Updates the private alias of the counterparty (AKA other account) OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.""",
+ Extraction.decompose(AliasJSON("An Alias")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update private alias of other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "private_alias" :: Nil JsonPut json -> _ => {
@@ -504,6 +1188,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyPrivateAlias,
+ apiVersion,
+ "deleteCounterpartyPrivateAlias",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/private_alias",
+ "Delete Counterparty Private Alias",
+ s"""Deletes the private alias of the other account OTHER_ACCOUNT_ID.
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyPrivateAlias : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete private alias of other bank account
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "private_alias" :: Nil JsonDelete _ => {
@@ -521,9 +1224,25 @@ trait APIMethods121 {
//TODO: get more info of counterparty?
+ resourceDocs += ResourceDoc(
+ addCounterpartyMoreInfo,
+ apiVersion,
+ "addCounterpartyMoreInfo",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/more_info",
+ "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,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add more info to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "more_info" :: Nil JsonPost json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "more_info" :: Nil JsonPost json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -540,9 +1259,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyMoreInfo,
+ apiVersion,
+ "updateCounterpartyMoreInfo",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/more_info",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update more info of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "more_info" :: Nil JsonPut json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "more_info" :: Nil JsonPut json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -559,9 +1294,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyMoreInfo,
+ apiVersion,
+ "deleteCounterpartyMoreInfo",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/more_info",
+ "Delete more info of other bank account.",
+ "",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyMoreInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete more info of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "more_info" :: Nil JsonDelete _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "more_info" :: Nil JsonDelete _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -576,9 +1327,26 @@ trait APIMethods121 {
//TODO: get url of counterparty?
+ resourceDocs += ResourceDoc(
+ addCounterpartyUrl,
+ apiVersion,
+ "addCounterpartyUrl",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/url",
+ "Add url to other bank account.",
+ "A url which represents the counterparty (home page url etc.)",
+ Extraction.decompose(UrlJSON("www.example.com")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
+
lazy val addCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add url to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "url" :: Nil JsonPost json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "url" :: Nil JsonPost json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -595,9 +1363,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyUrl,
+ apiVersion,
+ "updateCounterpartyUrl",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/url",
+ "Update url of other bank account.",
+ "A url which represents the counterparty (home page url etc.)",
+ Extraction.decompose(UrlJSON("www.example.com")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update url of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "url" :: Nil JsonPut json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "url" :: Nil JsonPut json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -614,9 +1398,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyUrl,
+ apiVersion,
+ "deleteCounterpartyUrl",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/url",
+ "Delete url of other bank account.",
+ "",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete url of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "url" :: Nil JsonDelete _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "url" :: Nil JsonDelete _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -631,9 +1431,25 @@ trait APIMethods121 {
//TODO: get image url of counterparty?
+ resourceDocs += ResourceDoc(
+ addCounterpartyImageUrl,
+ apiVersion,
+ "addCounterpartyImageUrl",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/image_url",
+ "Add image url to other bank account.",
+ "Add a url that points to the logo of the counterparty",
+ Extraction.decompose(ImageUrlJSON("www.example.com/logo.png")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add image url to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "image_url" :: Nil JsonPost json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "image_url" :: Nil JsonPost json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -650,9 +1466,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyImageUrl,
+ apiVersion,
+ "updateCounterpartyImageUrl",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/image_url",
+ "Update Counterparty Image Url",
+ "Update the url that points to the logo of the counterparty",
+ Extraction.decompose(ImageUrlJSON("www.example.com/logo.png")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update image url of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "image_url" :: Nil JsonPut json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "image_url" :: Nil JsonPut json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -669,9 +1501,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyImageUrl,
+ apiVersion,
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty)) // Tag general then specific for consistent sorting
+
lazy val deleteCounterpartyImageUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete image url of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "image_url" :: Nil JsonDelete _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "image_url" :: Nil JsonDelete _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -686,9 +1534,25 @@ trait APIMethods121 {
//TODO: get open corporates url of counterparty?
+ resourceDocs += ResourceDoc(
+ addCounterpartyOpenCorporatesUrl,
+ apiVersion,
+ "addCounterpartyOpenCorporatesUrl",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/open_corporates_url",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add open corporate url to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "open_corporates_url" :: Nil JsonPost json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "open_corporates_url" :: Nil JsonPost json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -696,8 +1560,8 @@ trait APIMethods121 {
otherBankAccount <- account.moderatedOtherBankAccount(other_account_id, view, user)
metadata <- Box(otherBankAccount.metadata) ?~ {"the view " + viewId + "does not allow metadata access"}
addOpenCorpUrl <- Box(metadata.addOpenCorporatesURL) ?~ {"the view " + viewId + "does not allow adding an open corporate url"}
- opernCoprUrl <- tryo{(json.extract[OpenCorporateUrlJSON])} ?~ {"wrong JSON format"}
- if(addOpenCorpUrl(opernCoprUrl.open_corporates_URL))
+ openCorpUrl <- tryo{(json.extract[OpenCorporateUrlJSON])} ?~ {"wrong JSON format"}
+ if(addOpenCorpUrl(openCorpUrl.open_corporates_URL))
} yield {
val successJson = SuccessMessage("open corporate url added")
successJsonResponse(Extraction.decompose(successJson), 201)
@@ -705,9 +1569,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyOpenCorporatesUrl,
+ apiVersion,
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update open corporate url of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "open_corporates_url" :: Nil JsonPut json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "open_corporates_url" :: Nil JsonPut json -> _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -715,8 +1595,8 @@ trait APIMethods121 {
otherBankAccount <- account.moderatedOtherBankAccount(other_account_id, view, user)
metadata <- Box(otherBankAccount.metadata) ?~ {"the view " + viewId + "does not allow metadata access"}
addOpenCorpUrl <- Box(metadata.addOpenCorporatesURL) ?~ {"the view " + viewId + "does not allow updating an open corporate url"}
- opernCoprUrl <- tryo{(json.extract[OpenCorporateUrlJSON])} ?~ {"wrong JSON format"}
- if(addOpenCorpUrl(opernCoprUrl.open_corporates_URL))
+ openCorpUrl <- tryo{(json.extract[OpenCorporateUrlJSON])} ?~ {"wrong JSON format"}
+ if(addOpenCorpUrl(openCorpUrl.open_corporates_URL))
} yield {
val successJson = SuccessMessage("open corporate url updated")
successJsonResponse(Extraction.decompose(successJson))
@@ -724,9 +1604,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyOpenCorporatesUrl,
+ apiVersion,
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyOpenCorporatesUrl : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete open corporate url of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "open_corporates_url" :: Nil JsonDelete _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "open_corporates_url" :: Nil JsonDelete _ => {
user =>
for {
account <- BankAccount(bankId, accountId)
@@ -740,9 +1636,26 @@ trait APIMethods121 {
}
//TODO: get corporate location of counterparty?
+
+ resourceDocs += ResourceDoc(
+ addCounterpartyCorporateLocation,
+ apiVersion,
+ "addCounterpartyCorporateLocation",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/corporate_location",
+ "Add Corporate Location to Counterparty",
+ "Add the geolocation of the counterparty's registered address",
+ Extraction.decompose(CorporateLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add corporate location to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts" :: other_account_id :: "corporate_location" :: Nil JsonPost json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts" :: other_account_id :: "metadata" :: "corporate_location" :: Nil JsonPost json -> _ => {
user =>
for {
u <- user
@@ -761,9 +1674,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyCorporateLocation,
+ apiVersion,
+ "updateCounterpartyCorporateLocation",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/corporate_location",
+ "Update Counterparty Corporate Location",
+ "Update the geolocation of the counterparty's registered address",
+ Extraction.decompose(CorporateLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update corporate location of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "corporate_location" :: Nil JsonPut json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "corporate_location" :: Nil JsonPut json -> _ => {
user =>
for {
u <- user
@@ -782,9 +1711,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyCorporateLocation,
+ apiVersion,
+ "deleteCounterpartyCorporateLocation",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/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,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyCorporateLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete corporate location of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "corporate_location" :: Nil JsonDelete _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "corporate_location" :: Nil JsonDelete _ => {
user =>
for {
u <- user
@@ -803,9 +1748,26 @@ trait APIMethods121 {
}
//TODO: get physical location of counterparty?
+
+ resourceDocs += ResourceDoc(
+ addCounterpartyPhysicalLocation,
+ apiVersion,
+ "addCounterpartyPhysicalLocation",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/physical_location",
+ "Add physical location to other bank account.",
+ "Add geocoordinates of the counterparty's main location",
+ Extraction.decompose(PhysicalLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val addCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add physical location to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts" :: other_account_id :: "physical_location" :: Nil JsonPost json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts" :: other_account_id :: "metadata" :: "physical_location" :: Nil JsonPost json -> _ => {
user =>
for {
u <- user
@@ -824,9 +1786,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateCounterpartyPhysicalLocation,
+ apiVersion,
+ "updateCounterpartyPhysicalLocation",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/physical_location",
+ "Update Counterparty Physical Location",
+ "Update geocoordinates of the counterparty's main location",
+ Extraction.decompose(PhysicalLocationJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val updateCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update physical location to other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "physical_location" :: Nil JsonPut json -> _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "physical_location" :: Nil JsonPut json -> _ => {
user =>
for {
u <- user
@@ -845,9 +1823,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteCounterpartyPhysicalLocation,
+ apiVersion,
+ "deleteCounterpartyPhysicalLocation",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/other_accounts/OTHER_ACCOUNT_ID/metadata/physical_location",
+ "Delete Counterparty Physical Location.",
+ "Delete physical location of other bank account.",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagCounterparty))
+
lazy val deleteCounterpartyPhysicalLocation : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete physical location of other bank account
- case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "physical_location" :: Nil JsonDelete _ => {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "other_accounts":: other_account_id :: "metadata" :: "physical_location" :: Nil JsonDelete _ => {
user =>
for {
u <- user
@@ -865,6 +1859,35 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getTransactionsForBankAccount,
+ apiVersion,
+ "getTransactionsForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/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.
+ |
+ |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,
+ false,
+ true,
+ false,
+ List(apiTagAccount, apiTagTransaction))
+
lazy val getTransactionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get transactions
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: Nil JsonGet json => {
@@ -882,6 +1905,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getTransactionByIdForBankAccount,
+ apiVersion,
+ "getTransactionByIdForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/transaction",
+ "Get transaction by id.",
+ """Returns one transaction specified by TRANSACTION_ID of the account ACCOUNT_ID and [moderated](#1_2_1-getViewsForBankAccount) by the view (VIEW_ID).
+ |
+ |Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ true,
+ false,
+ List(apiTagAccount, apiTagTransaction))
+
lazy val getTransactionByIdForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get transaction by id
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "transaction" :: Nil JsonGet json => {
@@ -897,6 +1938,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getTransactionNarrative,
+ apiVersion,
+ "getTransactionNarrative",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative",
+ "Get narrative.",
+ """Returns the account owner description of the transaction [moderated](#1_2_1-getViewsForBankAccount) by the view.
+ |
+ |Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val getTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get narrative
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "narrative" :: Nil JsonGet json => {
@@ -911,6 +1970,29 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addTransactionNarrative,
+ apiVersion,
+ "addTransactionNarrative",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative",
+ "Add narrative.",
+ s"""Creates a description of the transaction TRANSACTION_ID.
+ |
+ |Note: Unlike other items of metadata, there is only one "narrative" per transaction accross all views.
+ |If you set narrative via a view e.g. view-x it will be seen via view-y (as long as view-y has permission to see the narrative).
+ |
+ |${authenticationRequiredMessage(false)}
+ |Authentication is required if the view is not public.
+ |""",
+ Extraction.decompose(TransactionNarrativeJSON("My new (old!) piano")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val addTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add narrative
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "narrative" :: Nil JsonPost json -> _ => {
@@ -928,6 +2010,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateTransactionNarrative,
+ apiVersion,
+ "updateTransactionNarrative",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative",
+ "Update narrative.",
+ """Updates the description of the transaction TRANSACTION_ID.
+ |
+ |Authentication via OAuth is required if the view is not public.""",
+ Extraction.decompose(TransactionNarrativeJSON("My new (old!) piano")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val updateTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update narrative
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "narrative" :: Nil JsonPut json -> _ => {
@@ -945,6 +2045,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteTransactionNarrative,
+ apiVersion,
+ "deleteTransactionNarrative",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative",
+ "Delete narrative.",
+ """Deletes the description of the transaction TRANSACTION_ID.
+ |
+ |Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val deleteTransactionNarrative : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete narrative
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "narrative" :: Nil JsonDelete _ => {
@@ -959,6 +2077,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCommentsForViewOnTransaction,
+ apiVersion,
+ "getCommentsForViewOnTransaction",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments",
+ "Get comments.",
+ """Returns the transaction TRANSACTION_ID comments made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID).
+ |
+ |Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val getCommentsForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get comments
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "comments" :: Nil JsonGet json => {
@@ -973,6 +2109,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addCommentForViewOnTransaction,
+ apiVersion,
+ "addCommentForViewOnTransaction",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments",
+ "Add comment.",
+ """Posts a comment about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID.
+ |
+ |${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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val addCommentForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add comment
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "comments" :: Nil JsonPost json -> _ => {
@@ -989,6 +2145,26 @@ trait APIMethods121 {
}
}
+ // Not able to update a comment (delete and add another)
+
+ resourceDocs += ResourceDoc(
+ deleteCommentForViewOnTransaction,
+ apiVersion,
+ "deleteCommentForViewOnTransaction",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments/COMMENT_ID",
+ "Delete comment.",
+ """Delete the comment COMMENT_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 comment.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val deleteCommentForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete comment
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "comments":: commentId :: Nil JsonDelete _ => {
@@ -1003,6 +2179,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getTagsForViewOnTransaction,
+ apiVersion,
+ "getTagsForViewOnTransaction",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags",
+ "Get tags.",
+ """Returns the transaction TRANSACTION_ID tags made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID).
+
+Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val getTagsForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get tags
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "tags" :: Nil JsonGet json => {
@@ -1017,6 +2211,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addTagForViewOnTransaction,
+ apiVersion,
+ "addTagForViewOnTransaction",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags",
+ "Add a tag.",
+ s"""Posts a tag about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID.
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |Authentication is required as the tag is linked with the user.""",
+ Extraction.decompose(PostTransactionTagJSON("holiday")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val addTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add a tag
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "tags" :: Nil JsonPost json -> _ => {
@@ -1024,7 +2238,7 @@ trait APIMethods121 {
user =>
for {
u <- user
- tagJson <- tryo{json.extract[PostTransactionTagJSON]}
+ tagJson <- tryo{json.extract[PostTransactionTagJSON]} // TODO Error handling
metadata <- moderatedTransactionMetadata(bankId, accountId, viewId, transactionId, Full(u))
addTagFunc <- Box(metadata.addTag) ?~ {"view " + viewId + " does not authorize adding tags"}
postedTag <- addTagFunc(u.apiId, viewId, tagJson.value, now)
@@ -1034,6 +2248,26 @@ trait APIMethods121 {
}
}
+ // No update tag (delete and add another)
+
+ resourceDocs += ResourceDoc(
+ deleteTagForViewOnTransaction,
+ apiVersion,
+ "deleteTagForViewOnTransaction",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags/TAG_ID",
+ "Delete a tag.",
+ """Deletes the tag TAG_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 tag.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val deleteTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete a tag
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "tags" :: tagId :: Nil JsonDelete _ => {
@@ -1049,6 +2283,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getImagesForViewOnTransaction,
+ apiVersion,
+ "getImagesForViewOnTransaction",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images",
+ "Get images.",
+ """Returns the transaction TRANSACTION_ID images made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID).
+
+Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val getImagesForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get images
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "images" :: Nil JsonGet json => {
@@ -1063,6 +2315,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addImageForViewOnTransaction,
+ apiVersion,
+ "addImageForViewOnTransaction",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images",
+ "Add an image.",
+ s"""Posts an image about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID.
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |The image is linked with the user.""",
+ Extraction.decompose(PostTransactionImageJSON("The new printer", "www.example.com/images/printer.png")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val addImageForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add an image
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "images" :: Nil JsonPost json -> _ => {
@@ -1080,6 +2352,24 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteImageForViewOnTransaction,
+ apiVersion,
+ "deleteImageForViewOnTransaction",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images/IMAGE_ID",
+ "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.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val deleteImageForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete an image
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "images" :: imageId :: Nil JsonDelete _ => {
@@ -1094,6 +2384,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getWhereTagForViewOnTransaction,
+ apiVersion,
+ "getWhereTagForViewOnTransaction",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where",
+ "Get where tag.",
+ """Returns the "where" Geo tag added to the transaction TRANSACTION_ID made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID).
+ |It represents the location where the transaction has been initiated.
+ |
+ |Authentication via OAuth is required if the view is not public.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val getWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get where tag
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "where" :: Nil JsonGet json => {
@@ -1109,6 +2418,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ addWhereTagForViewOnTransaction,
+ apiVersion,
+ "addWhereTagForViewOnTransaction",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where",
+ "Add where tag.",
+ s"""Creates a "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount).
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |The geo tag is linked with the user.""",
+ Extraction.decompose(PostTransactionWhereJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val addWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//add where tag
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "where" :: Nil JsonPost json -> _ => {
@@ -1128,6 +2457,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ updateWhereTagForViewOnTransaction,
+ apiVersion,
+ "updateWhereTagForViewOnTransaction",
+ "PUT",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where",
+ "Update where tag.",
+ s"""Updates the "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount).
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |The geo tag is linked with the user.""",
+ Extraction.decompose(PostTransactionWhereJSON(JSONFactory.createLocationPlainJSON(52.5571573,13.3728025))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val updateWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//update where tag
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "where" :: Nil JsonPut json -> _ => {
@@ -1147,6 +2496,26 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ deleteWhereTagForViewOnTransaction,
+ apiVersion,
+ "deleteWhereTagForViewOnTransaction",
+ "DELETE",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where",
+ "Delete where 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,
+ false,
+ false,
+ false,
+ List(apiTagMetaData, apiTagTransaction))
+
lazy val deleteWhereTagForViewOnTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//delete where tag
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: TransactionId(transactionId) :: "metadata" :: "where" :: Nil JsonDelete _ => {
@@ -1165,6 +2534,25 @@ trait APIMethods121 {
}
}
+ resourceDocs += ResourceDoc(
+ getCounterpartyForTransaction,
+ apiVersion,
+ "getCounterpartyForTransaction",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/other_account",
+ "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,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagTransaction, apiTagCounterparty))
+
lazy val getCounterpartyForTransaction : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//get other account of a transaction
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions":: TransactionId(transactionId) :: "other_account" :: Nil JsonGet json => {
@@ -1184,17 +2572,37 @@ trait APIMethods121 {
case class TransactionIdJson(transaction_id : String)
+ resourceDocs += ResourceDoc(
+ makePayment,
+ apiVersion,
+ "makePayment",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions",
+ "Make Payment.",
+ """This is an experimental call, currently only implemented in the OBP sandbox instance up to version 1.3.0. It is very minimal
+ |and was superseded by Transaction Requests in version 1.4.0.
+ |
+ |This will only work if account to pay exists at the bank specified in the json, and if that account has the same currency as that of the payee.
+ |
+ |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,
+ false,
+ false,
+ false,
+ List(apiTagTransactionRequest))
+
lazy val makePayment : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: Nil JsonPost json -> _ => {
user =>
-
if (Props.getBool("payments_enabled", false)) {
for {
u <- user ?~ "User not found"
makeTransJson <- tryo{json.extract[MakePaymentJson]} ?~ {"wrong json format"}
rawAmt <- tryo {BigDecimal(makeTransJson.amount)} ?~! s"amount ${makeTransJson.amount} not convertible to number"
toAccountUID = BankAccountUID(BankId(makeTransJson.bank_id), AccountId(makeTransJson.account_id))
- createdPaymentId <- Connector.connector.vend.makePayment(u, BankAccountUID(bankId, accountId), toAccountUID, rawAmt)
+ createdPaymentId <- Connector.connector.vend.makePayment(u, BankAccountUID(bankId, accountId), toAccountUID, rawAmt, "")
} yield {
val successJson = Extraction.decompose(TransactionIdJson(createdPaymentId.value))
successJsonResponse(successJson)
diff --git a/src/main/scala/code/api/1_2_1/JSONFactory1.2.1.scala b/src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.scala
similarity index 98%
rename from src/main/scala/code/api/1_2_1/JSONFactory1.2.1.scala
rename to src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.scala
index b6416111e..63bb94953 100644
--- a/src/main/scala/code/api/1_2_1/JSONFactory1.2.1.scala
+++ b/src/main/scala/code/api/v1_2_1/JSONFactory1.2.1.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
@@ -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]
@@ -144,6 +144,13 @@ case class AccountJSON(
views_available : List[ViewJSON],
bank_id : String
)
+
+case class UpdateAccountJSON(
+ id : String,
+ label : String,
+ bank_id : String
+)
+
case class ModeratedAccountJSON(
id : String,
label : String,
@@ -360,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
)
}
diff --git a/src/main/scala/code/api/1_2_1/OBPAPI1.2.1.scala b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala
similarity index 96%
rename from src/main/scala/code/api/1_2_1/OBPAPI1.2.1.scala
rename to src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.scala
index f15d36484..7567a792c 100644
--- a/src/main/scala/code/api/1_2_1/OBPAPI1.2.1.scala
+++ b/src/main/scala/code/api/v1_2_1/OBPAPI1.2.1.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
@@ -36,6 +36,8 @@ 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
+
object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable {
@@ -43,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,
@@ -52,6 +54,7 @@ object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 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,
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 c4173b201..23c5596f6 100644
--- a/src/main/scala/code/api/v1_3_0/APIMethods130.scala
+++ b/src/main/scala/code/api/v1_3_0/APIMethods130.scala
@@ -8,6 +8,15 @@ import code.model.{BankId, PhysicalCard, User}
import code.bankconnectors.Connector
import net.liftweb.json.Extraction
import APIUtil._
+import net.liftweb.json.JsonAST.JValue
+
+import scala.collection.mutable.ArrayBuffer
+
+import scala.collection.immutable.Nil
+
+// Makes JValue assignment to Nil work
+import net.liftweb.json.JsonDSL._
+
trait APIMethods130 {
//needs to be a RestHelper to get access to JsonGet, JsonPost, etc.
@@ -15,6 +24,27 @@ trait APIMethods130 {
val Implementations1_3_0 = new Object(){
+ val resourceDocs = ArrayBuffer[ResourceDoc]()
+ val emptyObjectJson : JValue = Nil
+ val apiVersion : String = "1_3_0"
+
+
+ resourceDocs += ResourceDoc(
+ getCards,
+ apiVersion,
+ "getCards",
+ "GET",
+ "/cards",
+ "Get cards for the current user",
+ "Returns data about all the physical cards a user has been issued. These could be debit cards, credit cards, etc.",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer))
+
lazy val getCards : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "cards" :: Nil JsonGet _ => {
user => {
@@ -31,6 +61,24 @@ trait APIMethods130 {
}
}
+
+ resourceDocs += ResourceDoc(
+ getCardsForBank,
+ apiVersion,
+ "getCardsForBank",
+ "GET",
+ "/banks/BANK_ID/cards",
+ "Get cards for the specified bank",
+ "",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer))
+
+
def getCardsForBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "cards" :: Nil JsonGet _ => {
user => {
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 0303169a1..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
@@ -4,6 +4,9 @@ import code.api.OBPRestHelper
import code.api.v1_2_1.APIMethods121
import net.liftweb.common.Loggable
+
+// 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 {
@@ -13,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,
@@ -81,7 +84,7 @@ object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 w
Implementations1_2_1.updateWhereTagForViewOnTransaction,
Implementations1_2_1.deleteWhereTagForViewOnTransaction,
Implementations1_2_1.getCounterpartyForTransaction,
- Implementations1_2_1.makePayment, //TODO: add v1.3.0 "challenges"
+ Implementations1_2_1.makePayment,
Implementations1_3_0.getCards,
Implementations1_3_0.getCardsForBank
)
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..69851d49f 100644
--- a/src/main/scala/code/api/v1_4_0/APIMethods140.scala
+++ b/src/main/scala/code/api/v1_4_0/APIMethods140.scala
@@ -1,46 +1,135 @@
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.customerinfo.{CustomerMessages, CustomerInfo}
-import code.model.{BankId, User}
-import net.liftweb.common.Box
+import java.text.SimpleDateFormat
+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 code.usercustomerlinks.UserCustomerLink
+import net.liftweb.common.{Failure, Loggable, Box, Full}
import net.liftweb.http.js.JE.JsRaw
import net.liftweb.http.{JsonResponse, Req}
import net.liftweb.http.rest.RestHelper
-import code.api.util.APIUtil._
-import net.liftweb.json.Extraction
-import net.liftweb.json.JsonAST.JObject
+import net.liftweb.json.{ShortTypeHints, DefaultFormats, Extraction}
+import net.liftweb.json.JsonAST.{JField, JObject, JValue}
import net.liftweb.util.Helpers.tryo
-import code.util.Helper._
+import net.liftweb.json.JsonDSL._
+import net.liftweb.util.Props
+import net.liftweb.json.JsonAST.JValue
-trait APIMethods140 {
+
+import code.api.v1_2_1.{AmountOfMoneyJSON}
+
+import scala.collection.immutable.Nil
+
+// JObject creation
+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}
+
+// 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
+import code.crm.CrmEvent
+import code.customer.{MockCustomerFaceImage, CustomerMessages, Customer}
+import code.model._
+import code.products.Products
+import code.api.util.APIUtil._
+import code.api.util.ErrorMessages
+
+import code.util.Helper._
+import code.api.util.APIUtil.ResourceDoc
+import java.text.SimpleDateFormat
+
+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
self: RestHelper =>
+ val Implementations1_4_0 = new Object() {
- val Implementations1_4_0 = new Object(){
+ val resourceDocs = ArrayBuffer[ResourceDoc]()
+ val emptyObjectJson : JValue = Nil
+ val apiVersion : String = "1_4_0"
- lazy val getCustomerInfo : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ val exampleDateString : String ="22/08/2013"
+ val simpleDateFormat : SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy")
+ val exampleDate = simpleDateFormat.parse(exampleDateString)
+
+
+ resourceDocs += ResourceDoc(
+ getCustomer,
+ apiVersion,
+ "getCustomer",
+ "GET",
+ "/banks/BANK_ID/customer",
+ "Get customer 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 getCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "customer" :: Nil JsonGet _ => {
user => {
for {
- u <- user ?~! "User must be logged in to retrieve customer info"
- info <- CustomerInfo.customerInfoProvider.vend.getInfo(bankId, u) ~> APIFailure("No customer info found", 404)
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound}
+ 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.createCustomerInfoJson(info)
+ val json = JSONFactory1_4_0.createCustomerJson(info)
successJsonResponse(Extraction.decompose(json))
}
}
}
}
+ resourceDocs += ResourceDoc(
+ getCustomerMessages,
+ apiVersion,
+ "getCustomerMessages",
+ "GET",
+ "/banks/BANK_ID/customer/messages",
+ "Get Customer Messages (current)",
+ """Get messages for the logged in customer
+ |Messages sent to the currently authenticated user.
+ |
+ |Authentication via OAuth is required.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagCustomer))
+
lazy val getCustomerMessages : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "customer" :: "messages" :: Nil JsonGet _ => {
user => {
for {
- u <- user ?~! "User must be logged in to retrieve customer messages"
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ 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 {
val messages = CustomerMessages.customerMessageProvider.vend.getMessages(u, bankId)
val json = JSONFactory1_4_0.createCustomerMessagesJson(messages)
@@ -50,15 +139,36 @@ trait APIMethods140 {
}
}
+ resourceDocs += ResourceDoc(
+ addCustomerMessage,
+ apiVersion,
+ "addCustomerMessage",
+ "POST",
+ "/banks/BANK_ID/customer/CUSTOMER_ID/messages",
+ "Add Customer Message.",
+ "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,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagCustomer)
+ )
+
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"
- customer <- CustomerInfo.customerInfoProvider.vend.getUser(bankId, customerNumber) ?~! "No customer found"
+ bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound}
+ 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)
@@ -67,12 +177,46 @@ trait APIMethods140 {
}
}
+
+ val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true)
+
+ resourceDocs += ResourceDoc(
+ getBranches,
+ apiVersion,
+ "getBranches",
+ "GET",
+ "/banks/BANK_ID/branches",
+ "Get Bank Branches",
+ 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
+ |
+ |${authenticationRequiredMessage(!getBranchesIsPublic)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ false,
+ true,
+ List(apiTagBank)
+ )
+
lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => {
user => {
for {
- branches <- Box(BankBranches.bankBranchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branch data available", 404)
+ u <- if(getBranchesIsPublic)
+ Box(Some(1))
+ else
+ user ?~! "User must be logged in to retrieve Branches data"
+ 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 {
+ // Format the data as json
val json = JSONFactory1_4_0.createBranchesJson(branches)
successJsonResponse(Extraction.decompose(json))
}
@@ -81,6 +225,461 @@ trait APIMethods140 {
}
- }
+ val getAtmsIsPublic = Props.getBool("apiOptions.getAtmsIsPublic", true)
+ resourceDocs += ResourceDoc(
+ getAtms,
+ apiVersion,
+ "getAtms",
+ "GET",
+ "/banks/BANK_ID/atms",
+ "Get Bank ATMS",
+ 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
+ |
+ |${authenticationRequiredMessage(!getAtmsIsPublic)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ false,
+ true,
+ List(apiTagBank)
+ )
+
+ lazy val getAtms : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "atms" :: Nil JsonGet _ => {
+ user => {
+ for {
+ // Get atms from the active provider
+
+ u <- if(getAtmsIsPublic)
+ Box(Some(1))
+ else
+ user ?~! "User must be logged in to retrieve ATM data"
+ 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
+ val json = JSONFactory1_4_0.createAtmsJson(atms)
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+
+ val getProductsIsPublic = Props.getBool("apiOptions.getProductsIsPublic", true)
+
+
+ resourceDocs += ResourceDoc(
+ getProducts,
+ apiVersion,
+ "getProducts",
+ "GET",
+ "/banks/BANK_ID/products",
+ "Get Bank Products",
+ s"""Returns information about the financial products offered by a bank specified by BANK_ID including:
+ |
+ |* Name
+ |* Code
+ |* Category
+ |* Family
+ |* Super Family
+ |* More info URL
+ |* Description
+ |* Terms and Conditions
+ |* License the data under this endpoint is released under
+ |${authenticationRequiredMessage(!getProductsIsPublic)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ false,
+ true,
+ List(apiTagBank)
+ )
+
+ lazy val getProducts : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "products" :: Nil JsonGet _ => {
+ user => {
+ for {
+ // Get products from the active provider
+ u <- if(getProductsIsPublic)
+ Box(Some(1))
+ else
+ user ?~! "User must be logged in to retrieve Products data"
+ 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
+ val json = JSONFactory1_4_0.createProductsJson(products)
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ getCrmEvents,
+ apiVersion,
+ "getCrmEvents",
+ "GET",
+ "/banks/BANK_ID/crm-events",
+ "Get CRM Events for the logged in user",
+ "",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer)
+ )
+
+ lazy val getCrmEvents : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "crm-events" :: Nil JsonGet _ => {
+ user => {
+ for {
+ // Get crm events from the active provider
+ u <- user ?~! "User must be logged in to retrieve CRM Event information"
+ bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound}
+ crmEvents <- Box(CrmEvent.crmEventProvider.vend.getCrmEvents(bankId)) ~> APIFailure("No CRM Events available.", 204)
+ } yield {
+ // Format the data as json
+ val json = JSONFactory1_4_0.createCrmEventsJson(crmEvents)
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+ /*
+ transaction requests (new payments since 1.4.0)
+ */
+
+ resourceDocs += ResourceDoc(
+ getTransactionRequestTypes,
+ apiVersion,
+ "getTransactionRequestTypes",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types",
+ "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
+ |(as opposed to Transaction Types which include external types too e.g. for Transactions created by core banking etc.)
+ |
+ | A Transaction Request Type internally determines:
+ |
+ | * the required Transaction Request 'body' i.e. fields that define the 'what' and 'to' of a Transaction Request,
+ | * the type of security challenge that may be be raised before the Transaction Request proceeds, and
+ | * the threshold of that challenge.
+ |
+ | For instance in a 'SANDBOX_TAN' Transaction Request, for amounts over 1000 currency units, the user must supply a positive integer to complete the Transaction Request and create a Transaction.
+ |
+ | This approach aims to provide only one endpoint for initiating transactions, and one that handles challenges, whilst still allowing flexibility with the payload and internal logic.
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val getTransactionRequestTypes: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
+ Nil JsonGet _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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 {
+ val successJson = Extraction.decompose(transactionRequestTypes)
+ successJsonResponse(successJson)
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getTransactionRequests,
+ apiVersion,
+ "getTransactionRequests",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-requests",
+ "Get all Transaction Requests.",
+ "",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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)
+ }
+ yield {
+ // TODO return 1.4.0 version of Transaction Requests!
+ val successJson = Extraction.decompose(transactionRequests)
+ successJsonResponse(successJson)
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+
+
+ case class TransactionIdJson(transaction_id : String)
+
+ resourceDocs += ResourceDoc(
+ createTransactionRequest,
+ apiVersion,
+ "createTransactionRequest",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests",
+ "Create Transaction Request.",
+ """Initiate a Payment via a Transaction Request.
+ |
+ |This is the preferred method to create a payment and supersedes makePayment in 1.2.1.
+ |
+ |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow.
+ |
+ |In sandbox mode, if the amount is < 100 the transaction request will create a transaction without a challenge, else a challenge will need to be answered.""",
+ Extraction.decompose(TransactionRequestBodyJSON (
+ TransactionRequestAccountJSON("BANK_ID", "ACCOUNT_ID"),
+ AmountOfMoneyJSON("EUR", "100.53"),
+ "A description for the transaction to be created",
+ "one of the transaction types possible for the account"
+ )
+ ),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
+ TransactionRequestType(transactionRequestType) :: "transaction-requests" :: Nil JsonPost json -> _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ /* TODO:
+ * check if user has access using the view that is given (now it checks if user has access to owner view), will need some new permissions for transaction requests
+ * test: functionality, error messages if user not given or invalid, if any other value is not existing
+ */
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ transBodyJson <- tryo{json.extract[TransactionRequestBodyJSON]} ?~ {ErrorMessages.InvalidJsonFormat}
+ transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)}
+ 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 <- 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"
+ createdTransactionRequest <- Connector.connector.vend.createTransactionRequest(u, fromAccount, toAccount, transactionRequestType, transBody)
+ } yield {
+ val json = Extraction.decompose(createdTransactionRequest)
+ createdJsonResponse(json)
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ answerTransactionRequestChallenge,
+ apiVersion,
+ "answerTransactionRequestChallenge",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests/TRANSACTION_REQUEST_ID/challenge",
+ "Answer Transaction Request Challenge.",
+ "In Sandbox mode, any string that can be converted to a possitive integer will be accepted as an answer.",
+ Extraction.decompose(ChallengeAnswerJSON("89123812", "123345")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val answerTransactionRequestChallenge: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
+ TransactionRequestType(transactionRequestType) :: "transaction-requests" :: TransactionRequestId(transReqId) :: "challenge" :: Nil JsonPost json -> _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ u <- 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
+ answerOk <- Connector.connector.vend.answerTransactionRequestChallenge(transReqId, answerJson.answer)
+ //create transaction and insert its id into the transaction request
+ transactionRequest <- Connector.connector.vend.createTransactionAfterChallenge(u, transReqId)
+ } yield {
+ val successJson = Extraction.decompose(transactionRequest)
+ successJsonResponse(successJson, 202)
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+
+
+
+ resourceDocs += ResourceDoc(
+ addCustomer,
+ apiVersion,
+ "addCustomer",
+ "POST",
+ "/banks/BANK_ID/customer",
+ "Add a customer.",
+ 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
+ |${authenticationRequiredMessage(true)}
+ |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),
+ exampleDate, "Single", 1, List(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer))
+
+ lazy val addCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //updates a view on a bank account
+ case "banks" :: BankId(bankId) :: "customer" :: Nil JsonPost json -> _ => {
+ user =>
+ for {
+ u <- user ?~! "User must be logged in to post Customer"
+ 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
+ customer <- Customer.customerProvider.vend.addCustomer(bankId,
+ u,
+ 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 = JSONFactory1_4_0.createCustomerJson(customer)
+ successJsonResponse(Extraction.decompose(successJson))
+ }
+ }
+ }
+
+
+
+ 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,
+ false,
+ false,
+ false,
+ 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/JSONFactory1_4_0.scala b/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala
index 810369c9e..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
@@ -2,17 +2,59 @@ package code.api.v1_4_0
import java.util.Date
-import code.bankbranches.BankBranches
-import code.bankbranches.BankBranches.{BankBranch, DataLicense, BranchData}
-import code.customerinfo.{CustomerMessage, CustomerInfo}
+import code.api.util.APIUtil.ResourceDoc
+import code.common.{Meta, License, Location, Address}
+import code.atms.Atms.Atm
+import code.branches.Branches.{Branch}
+import code.crm.CrmEvent.{CrmEvent, CrmEventId}
+import code.products.Products.{Product}
+
+
+import code.customer.{CustomerMessage, Customer}
+import code.model._
+import code.products.Products.ProductCode
+import code.transactionrequests.TransactionRequests._
+import net.liftweb.json.JsonAST.{JValue, JObject}
+import org.pegdown.PegDownProcessor
+
+import code.api.v1_2_1.{AmountOfMoneyJSON}
+
object JSONFactory1_4_0 {
- case class CustomerInfoJson(customer_number : String,
- legal_name : String,
- mobile_phone_number : String,
- email : String,
- face_image : CustomerFaceImageJson)
+
+ 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,
+ 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 CustomerJSONs(customers: List[CustomerJson])
case class CustomerFaceImageJson(url : String, date : Date)
@@ -21,43 +63,348 @@ 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 BranchJson(id : String, name : String, address : AddressJson)
- 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 LicenseJson(id : String, name : String)
+
+ case class MetaJson(license : LicenseJson)
+
+ case class LocationJson(latitude : Double, longitude : Double)
+
+ case class DriveUpJson(hours : String)
+ case class LobbyJson(hours : String)
+
+
+
+ case class BranchJson(id : String,
+ name : String,
+ address : AddressJson,
+ location : LocationJson,
+ lobby : LobbyJson,
+ drive_up: DriveUpJson,
+ meta : MetaJson)
+
+ case class BranchesJson (branches : List[BranchJson])
+
+
+ case class AtmJson(id : String,
+ name : String,
+ address : AddressJson,
+ location : LocationJson,
+ meta : MetaJson)
+
+ case class AtmsJson (atms : List[AtmJson])
+
+
+ case class AddressJson(line_1 : String, line_2 : String, line_3 : String, city : String, state : String, postcode : String, country : String)
+
+
+
+
+
+ def createCustomerJson(cInfo : Customer) : CustomerJson = {
+
+ CustomerJson(
+ customer_id = cInfo.customerId,
+ customer_number = cInfo.number,
+ legal_name = cInfo.legalName,
+ mobile_phone_number = cInfo.mobileNumber,
+ email = cInfo.email,
+ face_image = CustomerFaceImageJson(url = cInfo.faceImage.url,
+ 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
+ )
- def createCustomerInfoJson(cInfo : CustomerInfo) : CustomerInfoJson = {
- CustomerInfoJson(customer_number = cInfo.number,
- legal_name = cInfo.legalName, mobile_phone_number = cInfo.mobileNumber,
- email = cInfo.email, face_image = CustomerFaceImageJson(url = cInfo.faceImage.url, date = cInfo.faceImage.date))
}
+ def createCustomersJson(customers : List[Customer]) : CustomerJSONs = {
+ CustomerJSONs(customers.map(createCustomerJson))
+ }
+
def createCustomerMessageJson(cMessage : CustomerMessage) : CustomerMessageJson = {
- CustomerMessageJson(id = cMessage.messageId, date = cMessage.date,
- message = cMessage.message, from_department = cMessage.fromDepartment,
- from_person = cMessage.fromPerson)
+ CustomerMessageJson(id = cMessage.messageId,
+ date = cMessage.date,
+ message = cMessage.message,
+ from_department = cMessage.fromDepartment,
+ from_person = cMessage.fromPerson)
}
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.id, license.name)
}
- def createAddressJson(address : BankBranches.Address) : AddressJson = {
- AddressJson(address.line1, address.line2, address.line3, address.line4, address.line5, address.postCode, address.countryCode)
+ def createLocationJson(location : Location) : LocationJson = {
+ LocationJson(location.latitude, location.longitude)
}
- def createBranchJson(bankBranch: BankBranch) : BranchJson = {
- BranchJson(bankBranch.branchId.value, bankBranch.name, createAddressJson(bankBranch.address))
+
+ def createDriveUpJson(hours : String) : DriveUpJson = {
+ DriveUpJson(hours)
}
- def createBranchesJson(branchData : BranchData) : BranchDataJson = {
- BranchDataJson(createDataLicenseJson(branchData.license), branchData.branches.map(createBranchJson))
+ def createLobbyJson(hours : String) : LobbyJson = {
+ LobbyJson(hours)
}
+ def createMetaJson(meta: Meta) : MetaJson = {
+ MetaJson(createLicenseJson(meta.license))
+ }
+
+
+ // Accept an address object and return its json representation
+ def createAddressJson(address : Address) : AddressJson = {
+ AddressJson(address.line1, address.line2, address.line3, address.city, address.state, address.postCode, address.countryCode)
+ }
+
+ // Branches
+
+ def createBranchJson(branch: Branch) : BranchJson = {
+ BranchJson(branch.branchId.value,
+ branch.name,
+ createAddressJson(branch.address),
+ createLocationJson(branch.location),
+ createLobbyJson(branch.lobby.hours),
+ createDriveUpJson(branch.driveUp.hours),
+ createMetaJson(branch.meta))
+ }
+
+ def createBranchesJson(branchesList: List[Branch]) : BranchesJson = {
+ BranchesJson(branchesList.map(createBranchJson))
+ }
+
+ // Atms
+
+ def createAtmJson(atm: Atm) : AtmJson = {
+ AtmJson(atm.atmId.value,
+ atm.name,
+ createAddressJson(atm.address),
+ createLocationJson(atm.location),
+ createMetaJson(atm.meta))
+ }
+
+ def createAtmsJson(AtmsList: List[Atm]) : AtmsJson = {
+ AtmsJson(AtmsList.map(createAtmJson))
+ }
+
+ // Products
+
+
+ case class ProductJson(code : String,
+ name : String,
+ category: String,
+ family : String,
+ super_family : String,
+ more_info_url: String,
+ meta : MetaJson)
+
+ case class ProductsJson (products : List[ProductJson])
+
+
+
+ def createProductJson(product: Product) : ProductJson = {
+ ProductJson(product.code.value,
+ product.name,
+ product.category,
+ product.family,
+ product.superFamily,
+ product.moreInfoUrl,
+ createMetaJson(product.meta))
+ }
+
+ def createProductsJson(productsList: List[Product]) : ProductsJson = {
+ ProductsJson(productsList.map(createProductJson))
+ }
+
+
+ // Crm Events
+ case class CrmEventJson(
+ id: String,
+ bank_id: String,
+ customer_name : String,
+ customer_number : String,
+ category : String,
+ detail : String,
+ channel : String,
+ scheduled_date : Date,
+ actual_date: Date,
+ result: String)
+
+ case class CrmEventsJson (crm_events : List[CrmEventJson])
+
+ def createCrmEventJson(crmEvent: CrmEvent) : CrmEventJson = {
+ CrmEventJson(
+ id = crmEvent.crmEventId.value,
+ bank_id = crmEvent.bankId.value,
+ customer_name = crmEvent.customerName,
+ customer_number = crmEvent.customerNumber,
+ category = crmEvent.category,
+ detail = crmEvent.detail,
+ channel = crmEvent.channel,
+ scheduled_date = crmEvent.scheduledDate,
+ actual_date = crmEvent.actualDate,
+ result = crmEvent.result)
+ }
+
+ def createCrmEventsJson(crmEventList: List[CrmEvent]) : CrmEventsJson = {
+ CrmEventsJson(crmEventList.map(createCrmEventJson))
+ }
+
+
+ // Used to describe where an API call is implemented
+ case class ImplementedByJson (
+ version : String, // Short hand for the version e.g. "1_4_0" means Implementations1_4_0
+ function : String // The val / partial function that implements the call e.g. "getBranches"
+ )
+
+
+ // Used to describe the OBP API calls for documentation and API discovery purposes
+ case class ResourceDocJson(operation_id: String,
+ request_verb: String,
+ request_url: String,
+ summary: String,
+ description: String,
+ example_request_body: JValue,
+ success_response_body: JValue,
+ 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(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
+ // Thus we use a flavour of markdown that ignores underscores in words. (Github markdown does this too)
+ // PegDown seems to be feature rich and ignores underscores in words by default.
+
+ // We return html rather than markdown to the consumer so they don't have to bother with these questions.
+
+ val pegDownProcessor : PegDownProcessor = new PegDownProcessor
+
+ ResourceDocJson(
+ 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(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.isOBWG, // No longer tracking isCore
+ tags = rd.tags.map(i => i.tag)
+ )
+ }
+
+ def createResourceDocsJson(resourceDocList: List[ResourceDoc]) : ResourceDocsJson = {
+ ResourceDocsJson(resourceDocList.map(createResourceDocJson))
+ }
+
+
+ //transaction requests
+ def getTransactionRequestBodyFromJson(body: TransactionRequestBodyJSON) : TransactionRequestBody = {
+ val toAcc = TransactionRequestAccount (
+ bank_id = body.to.bank_id,
+ account_id = body.to.account_id
+ )
+ val amount = AmountOfMoney (
+ currency = body.value.currency,
+ amount = body.value.amount
+ )
+
+ TransactionRequestBody (
+ to = toAcc,
+ value = amount,
+ description = body.description
+ )
+ }
+
+ def getTransactionRequestFromJson(json : TransactionRequestJSON) : TransactionRequest = {
+ val fromAcc = TransactionRequestAccount (
+ json.from.bank_id,
+ json.from.account_id
+ )
+ val challenge = TransactionRequestChallenge (
+ id = json.challenge.id,
+ allowed_attempts = json.challenge.allowed_attempts,
+ challenge_type = json.challenge.challenge_type
+ )
+
+ val charge = TransactionRequestCharge("Total charges for a completed transaction request.", AmountOfMoney(json.body.value.currency, "0.05"))
+
+
+ TransactionRequest (
+ id = TransactionRequestId(json.id),
+ `type`= json.`type`,
+ from = fromAcc,
+ body = getTransactionRequestBodyFromJson(json.body),
+ transaction_ids = json.transaction_ids,
+ status = json.status,
+ start_date = json.start_date,
+ end_date = json.end_date,
+ challenge = challenge,
+ charge = charge
+ )
+ }
+
+ case class TransactionRequestAccountJSON (
+ bank_id: String,
+ account_id : String
+ )
+
+ case class TransactionRequestBodyJSON (
+ to: TransactionRequestAccountJSON,
+ value : AmountOfMoneyJSON,
+ description : String,
+ challenge_type : String
+ )
+
+ case class TransactionRequestJSON(
+ id: String,
+ `type`: String,
+ from: TransactionRequestAccountJSON,
+ body: TransactionRequestBodyJSON,
+ transaction_ids: String,
+ status: String,
+ start_date: Date,
+ end_date: Date,
+ challenge: ChallengeJSON
+ )
+
+ case class ChallengeJSON (
+ id: String,
+ allowed_attempts : Int,
+ challenge_type: String
+ )
+
+ case class ChallengeAnswerJSON (
+ id: String,
+ answer : String
+ )
+
+ /*case class ChallengeErrorJSON (
+ code : Int,
+ message: 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 7feab3430..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
@@ -5,14 +5,99 @@ import net.liftweb.common.Loggable
object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable {
-
val VERSION = "1.4.0"
val routes = List(
- Implementations1_4_0.getCustomerInfo,
+ Implementations1_2_1.root(VERSION),
+ Implementations1_2_1.getBanks,
+ Implementations1_2_1.bankById,
+ 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,
+ 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, // Back for a while
+
+ // 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.getBranches,
+ Implementations1_4_0.getAtms,
+ Implementations1_4_0.getProducts,
+ Implementations1_4_0.getCrmEvents,
+ Implementations1_4_0.createTransactionRequest,
+ Implementations1_4_0.getTransactionRequests,
+ Implementations1_4_0.getTransactionRequestTypes,
+ Implementations1_4_0.answerTransactionRequestChallenge
+ )
routes.foreach(route => {
oauthServe(apiPrefix{route})
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..925e724aa
--- /dev/null
+++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala
@@ -0,0 +1,2145 @@
+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.ApiRole._
+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
+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
+import net.liftweb.mapper.By
+
+//import code.api.v1_4_0.JSONFactory1_4_0._
+import code.api.v2_0_0.JSONFactory200._
+import code.bankconnectors.Connector
+import code.fx.fx
+import code.kycchecks.KycChecks
+import code.kycdocuments.KycDocuments
+import code.kycmedias.KycMedias
+import code.kycstatuses.KycStatuses
+import code.model._
+import code.model.dataAccess.BankAccountCreation
+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
+import net.liftweb.http.{JsonResponse, Req}
+import net.liftweb.json.JsonAST.JValue
+import net.liftweb.util.Helpers._
+import net.liftweb.util.Props
+
+import scala.collection.immutable.Nil
+import scala.collection.mutable.ArrayBuffer
+// Makes JValue assignment to Nil work
+import code.customer.{MockCustomerFaceImage, Customer}
+import code.util.Helper._
+import net.liftweb.http.js.JE.JsRaw
+import net.liftweb.json.Extraction
+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
+
+
+ val defaultBankId = Props.get("defaultBank.bank_id", "DEFAULT_BANK_ID_NOT_SET")
+
+
+
+ // shows a small representation of View
+ private def bankAccountBasicListToJson(bankAccounts: List[BankAccount], user : Box[User]): JValue = {
+ Extraction.decompose(basicBankAccountList(bankAccounts, user))
+ }
+
+ // Shows accounts without view
+ private def coreBankAccountListToJson(callerContext: CallerContext, codeContext: CodeContext, bankAccounts: List[BankAccount], user : Box[User]): JValue = {
+ Extraction.decompose(coreBankAccountList(callerContext, codeContext, bankAccounts, user))
+ }
+
+ private def basicBankAccountList(bankAccounts: List[BankAccount], user : Box[User]): List[BasicAccountJSON] = {
+ val accJson : List[BasicAccountJSON] = bankAccounts.map(account => {
+ val views = account.permittedViews(user)
+ val viewsAvailable : List[BasicViewJSON] =
+ views.map( v => {
+ JSONFactory200.createViewBasicJSON(v)
+ })
+ JSONFactory200.createBasicAccountJSON(account,viewsAvailable)
+ })
+ accJson
+ }
+
+ private def coreBankAccountList(callerContext: CallerContext, codeContext: CodeContext, bankAccounts: List[BankAccount], user : Box[User]): List[CoreAccountJSON] = {
+ val accJson : List[CoreAccountJSON] = bankAccounts.map(account => {
+ val views = account.permittedViews(user)
+ val viewsAvailable : List[BasicViewJSON] =
+ views.map( v => {
+ JSONFactory200.createViewBasicJSON(v)
+ })
+
+ val dataContext = DataContext(user, Some(account.bankId), Some(account.accountId), Empty, Empty, Empty)
+
+ val links = code.api.util.APIUtil.getHalLinks(callerContext, codeContext, dataContext)
+
+ JSONFactory200.createCoreAccountJSON(account, links)
+ })
+ accJson
+ }
+
+
+
+ // helper methods end here
+
+ 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 exampleDateString: String = "22/08/2013"
+ val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy")
+ val exampleDate = simpleDateFormat.parse(exampleDateString)
+
+ val codeContext = CodeContext(resourceDocs, apiRelations)
+
+
+
+
+
+ resourceDocs += ResourceDoc(
+ allAccountsAllBanks,
+ apiVersion,
+ "allAccountsAllBanks",
+ "GET",
+ "/accounts",
+ "Get all Accounts at all Banks.",
+ 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,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagPrivateData, apiTagPublicData))
+
+
+ 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)))
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ privateAccountsAllBanks,
+ apiVersion,
+ "privateAccountsAllBanks",
+ "GET",
+ "/my/accounts",
+ "Get Accounts at all Banks (Private)",
+ 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.
+ |
+ |${authenticationRequiredMessage(true)}
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagAccount, apiTagPrivateData))
+
+
+ apiRelations += ApiRelation(privateAccountsAllBanks, getCoreAccountById, "detail")
+ apiRelations += ApiRelation(privateAccountsAllBanks, privateAccountsAllBanks, "self")
+
+
+
+ lazy val privateAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //get private accounts for all banks
+ case "my" :: "accounts" :: Nil JsonGet json => {
+ user =>
+
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ } yield {
+ val availableAccounts = BankAccount.nonPublicAccounts(u)
+ val coreBankAccountListJson = coreBankAccountListToJson(CallerContext(privateAccountsAllBanks), codeContext, availableAccounts, Full(u))
+ val response = successJsonResponse(coreBankAccountListJson)
+ response
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ publicAccountsAllBanks,
+ apiVersion,
+ "publicAccountsAllBanks",
+ "GET",
+ "/accounts/public",
+ "Get Public Accounts at all Banks.",
+ s"""Get public accounts at all banks (Anonymous access).
+ |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(false)}
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagPublicData))
+
+
+
+
+
+
+ lazy val publicAccountsAllBanks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //get public accounts for all banks
+ case "accounts" :: "public" :: Nil JsonGet json => {
+ user =>
+ for {
+ publicAccountsJson <- tryo{bankAccountBasicListToJson(BankAccount.publicAccounts, Empty)} ?~ "Could not get accounts."
+ } yield {
+ Full(successJsonResponse(publicAccountsJson))
+ }
+ }
+ }
+
+
+
+
+ resourceDocs += ResourceDoc(
+ allAccountsAtOneBank,
+ apiVersion,
+ "allAccountsAtOneBank",
+ "GET",
+ "/banks/BANK_ID/accounts",
+ "Get Accounts at one Bank (Public and Private).",
+ 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, the list will contain only the accounts providing public views.
+ """,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagPrivateData, apiTagPublicData)
+ )
+
+ 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) ?~! {ErrorMessages.BankNotFound}
+ } yield {
+ val availableAccounts = bank.accounts(user)
+ successJsonResponse(bankAccountBasicListToJson(availableAccounts, user))
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ privateAccountsAtOneBank,
+ apiVersion,
+ "privateAccountsAtOneBank",
+ "GET",
+ "/my/banks/BANK_ID/accounts",
+ "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.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagAccount, apiTagPrivateData))
+
+ apiRelations += ApiRelation(privateAccountsAtOneBank, createAccount, "new")
+ apiRelations += ApiRelation(privateAccountsAtOneBank, privateAccountsAtOneBank, "self")
+
+
+ def privateAccountsAtOneBankResult (bank: Bank, u: User) = {
+ val availableAccounts = bank.nonPublicAccounts(u)
+ successJsonResponse(bankAccountBasicListToJson(availableAccounts, Full(u)))
+ }
+
+ def corePrivateAccountsAtOneBankResult (callerContext: CallerContext, codeContext: CodeContext, bank: Bank, u: User) = {
+ val availableAccounts = bank.nonPublicAccounts(u)
+ successJsonResponse(coreBankAccountListToJson(callerContext, codeContext, availableAccounts, Full(u)))
+ }
+
+
+
+ // 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 "my" :: "banks" :: BankId(bankId) :: "accounts" :: Nil JsonGet json => {
+ user =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ bank <- Bank(bankId)
+
+ } yield {
+ corePrivateAccountsAtOneBankResult(CallerContext(privateAccountsAtOneBank), codeContext, bank, u)
+ }
+ }
+ case "bank" :: "accounts" :: Nil JsonGet json => {
+ println("in accounts")
+ user =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ bank <- Bank(BankId(defaultBankId))
+ } yield {
+ corePrivateAccountsAtOneBankResult(CallerContext(privateAccountsAtOneBank), codeContext, bank, u)
+ }
+ }
+
+ }
+
+ resourceDocs += ResourceDoc(
+ publicAccountsAtOneBank,
+ apiVersion,
+ "publicAccountsAtOneBank",
+ "GET",
+ "/banks/BANK_ID/accounts/public",
+ "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.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagPublicData))
+
+ 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)
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getKycDocuments,
+ apiVersion,
+ "getKycDocuments",
+ "GET",
+ "/customers/CUSTOMER_ID/kyc_documents",
+ "Get KYC Documents for 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.
+ |${authenticationRequiredMessage(false)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc))
+
+ lazy val getKycDocuments : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "customers" :: customerId :: "kyc_documents" :: Nil JsonGet _ => {
+ user => {
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId
+ } yield {
+ val kycDocuments = KycDocuments.kycDocumentProvider.vend.getKycDocuments(customer.number)
+ val json = JSONFactory200.createKycDocumentsJSON(kycDocuments)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ getKycMedia,
+ apiVersion,
+ "getKycMedia",
+ "GET",
+ "/customers/CUSTOMER_ID/kyc_media",
+ "Get KYC Media for a customer",
+ s"""Get KYC media (scans, pictures, videos) that affirms the identity of the customer.
+ |
+ |${authenticationRequiredMessage(true)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc))
+
+ lazy val getKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "customers" :: customerId :: "kyc_media" :: Nil JsonGet _ => {
+ user => {
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId
+ } yield {
+ val kycMedias = KycMedias.kycMediaProvider.vend.getKycMedias(customer.number)
+ val json = JSONFactory200.createKycMediasJSON(kycMedias)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getKycChecks,
+ apiVersion,
+ "getKycChecks",
+ "GET",
+ "/customers/CUSTOMER_ID/kyc_checks",
+ "Get KYC Checks for current Customer",
+ s"""Get KYC checks for the logged in customer
+ |Messages sent to the currently authenticated user.
+ |
+ |${authenticationRequiredMessage(true)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc))
+
+ lazy val getKycChecks : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "customers" :: customerId :: "kyc_checks" :: Nil JsonGet _ => {
+ user => {
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId
+ } yield {
+ val kycChecks = KycChecks.kycCheckProvider.vend.getKycChecks(customer.number)
+ val json = JSONFactory200.createKycChecksJSON(kycChecks)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+ resourceDocs += ResourceDoc(
+ getKycStatuses,
+ apiVersion,
+ "getKycStatuses",
+ "GET",
+ "/customers/CUSTOMER_ID/kyc_statuses",
+ "Get the KYC statuses for a customer",
+ s"""Get the KYC statuses for a customer over time
+ |
+ |${authenticationRequiredMessage(true)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc))
+
+ lazy val getKycStatuses : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "customers" :: customerId :: "kyc_statuses" :: Nil JsonGet _ => {
+ user => {
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId
+ } yield {
+ val kycStatuses = KycStatuses.kycStatusProvider.vend.getKycStatuses(customer.number)
+ val json = JSONFactory200.createKycStatusesJSON(kycStatuses)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getSocialMediaHandles,
+ apiVersion,
+ "getSocialMedia",
+ "GET",
+ "/banks/BANK_ID/customers/CUSTOMER_ID/social_media_handles",
+ "Get social media handles for a customer",
+ s"""Get social media handles for a customer.
+ |
+ |${authenticationRequiredMessage(true)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc))
+
+ lazy val getSocialMediaHandles : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ 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")
+ customer <- Customer.customerProvider.vend.getCustomerByCustomerId(customerId) ?~ ErrorMessages.CustomerNotFoundByCustomerId
+ } yield {
+ val kycSocialMedias = SocialMediaHandle.socialMediaHandleProvider.vend.getSocialMedias(customer.number)
+ val json = JSONFactory200.createSocialMediasJSON(kycSocialMedias)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+
+
+
+ resourceDocs += ResourceDoc(
+ addKycDocument,
+ apiVersion,
+ "addKycDocument",
+ "POST",
+ "/banks/BANK_ID/customers/CUSTOMER_ID/kyc_documents",
+ "Add KYC Document.",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc)
+ )
+
+ // 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" :: 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.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId
+ 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 KycDocument")
+ } yield {
+ successJsonResponse(JsRaw("{}"), 201)
+ }
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ addKycMedia,
+ apiVersion,
+ "addKycMedia",
+ "POST",
+ "/banks/BANK_ID/customers/CUSTOMER_ID/kyc_media",
+ "Add KYC Media.",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc)
+ )
+
+ lazy val addKycMedia : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ 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.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId
+ 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/customers/CUSTOMER_ID/kyc_check",
+ "Add KYC Check",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc)
+ )
+
+ lazy val addKycCheck : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ 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.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId
+ 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/customers/CUSTOMER_ID/kyc_statuses",
+ "Add KYC Status",
+ "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,
+ false,
+ false,
+ false,
+ List(apiTagCustomer, apiTagKyc)
+ )
+
+ lazy val addKycStatus : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ 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.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId
+ 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(
+ addSocialMediaHandle,
+ apiVersion,
+ "addSocialMediaHandle",
+ "POST",
+ "/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)),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagCustomer)
+ )
+
+ lazy val addSocialMediaHandle : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ 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 {
+ 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.getCustomerByCustomerId(customerId) ?~! ErrorMessages.CustomerNotFoundByCustomerId
+ kycSocialMediaCreated <- booleanToBox(
+ SocialMediaHandle.socialMediaHandleProvider.vend.addSocialMedias(
+ postedData.customer_number,
+ postedData.`type`,
+ postedData.handle,
+ postedData.date_added,
+ postedData.date_activated),
+ "Server error: could not add")
+ } yield {
+ successJsonResponse(JsRaw("{}"), 201)
+ }
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getCoreAccountById,
+ apiVersion,
+ "coreAccountById",
+ "GET",
+ "/my/banks/BANK_ID/accounts/ACCOUNT_ID/account",
+ "Get Account by Id (Core)",
+ """Information returned about the account specified by ACCOUNT_ID:
+ |
+ |* Number
+ |* Owners
+ |* Type
+ |* Balance
+ |* IBAN
+ |
+ |This call returns the owner view and requires access to that view.
+ |
+ |
+ |OAuth authentication is required""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ false,
+ apiTagAccount :: Nil)
+
+ lazy val getCoreAccountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //get account by id (assume owner view requested)
+ case "my" :: "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "account" :: Nil JsonGet json => {
+
+ 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))
+ // Assume owner view was requested
+ 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
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ getCoreTransactionsForBankAccount,
+ apiVersion,
+ "getCoreTransactionsForBankAccount",
+ "GET",
+ "/my/banks/BANK_ID/accounts/ACCOUNT_ID/transactions",
+ "Get Transactions for Account (Core)",
+ """Returns transactions list (Core info) 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,
+ true,
+ List(apiTagAccount, apiTagTransaction))
+
+ 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))
+ }
+ }
+ }
+
+
+ // Copied from 1.2.1 and modified
+
+ resourceDocs += ResourceDoc(
+ accountById,
+ apiVersion,
+ "accountById",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account",
+ "Get Account by Id (Full)",
+ """Information returned about an account specified by ACCOUNT_ID as moderated by the view (VIEW_ID):
+ |
+ |* Number
+ |* Owners
+ |* Type
+ |* Balance
+ |* IBAN
+ |* Available views (sorted by short_name)
+ |
+ |More details about the data moderation by the view [here](#1_2_1-getViewsForBankAccount).
+ |
+ |PSD2 Context: PSD2 requires customers to have access to their account information via third party applications.
+ |This call provides balance and other account information via delegated authenticaiton using OAuth.
+ |
+ |OAuth authentication is required if the 'is_public' field in view (VIEW_ID) is not set to `true`.
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ true,
+ false,
+ apiTagAccount :: Nil)
+
+ lazy val accountById : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //get account by id
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "account" :: Nil JsonGet json => {
+ user =>
+ for {
+ 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)
+ val moderatedAccountJson = JSONFactory121.createBankAccountJSON(moderatedAccount, viewsAvailable)
+ successJsonResponse(Extraction.decompose(moderatedAccountJson))
+ }
+ }
+ }
+
+
+ /////
+
+
+ resourceDocs += ResourceDoc(
+ getPermissionsForBankAccount,
+ apiVersion,
+ "getPermissionsForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions",
+ "Get access.",
+ """Returns the list of the permissions at BANK_ID for account ACCOUNT_ID, with each time a pair composed of the user and the views that he has access to.
+ |
+ |OAuth authentication is required and the user needs to have access to the owner view.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement)
+ )
+
+ lazy val getPermissionsForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //get access
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: Nil JsonGet json => {
+ user =>
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn // 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.
+ permissions <- account permissions u
+ } yield {
+ val permissionsJSON = JSONFactory121.createPermissionsJSON(permissions.sortBy(_.user.emailAddress))
+ successJsonResponse(Extraction.decompose(permissionsJSON))
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getPermissionForUserForBankAccount,
+ apiVersion,
+ "getPermissionForUserForBankAccount",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/permissions/PROVIDER_ID/USER_ID",
+ "Get access for specific user.",
+ """Returns the list of the views at BANK_ID for account ACCOUNT_ID that a USER_ID at their provider PROVIDER_ID has access to.
+ |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.""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, apiTagUser, apiTagAccount, apiTagView, apiTagEntitlement))
+
+ lazy val getPermissionForUserForBankAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ //get access for specific user
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "permissions" :: providerId :: userId :: Nil JsonGet json => {
+ user =>
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn // 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.
+ permission <- account permission(u, providerId, userId)
+ } yield {
+ val views = JSONFactory121.createViewsJSON(permission.views.sortBy(_.viewId.value))
+ successJsonResponse(Extraction.decompose(views))
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ createAccount,
+ apiVersion,
+ "createAccount",
+ "PUT",
+ "/banks/BANK_ID/accounts/NEW_ACCOUNT_ID",
+ "Create Account",
+ """Create Account at bank specified by BANK_ID with Id specified by NEW_ACCOUNT_ID.
+ |
+ |
+ |The User can create an Account for themself or an Account for another User if they have CanCreateAccount role.
+ |
+ |If USER_ID is not specified the account will be owned by the logged in User.
+ |
+ |Note: The Amount must be zero.""".stripMargin,
+ Extraction.decompose(CreateAccountJSON("A user_id","CURRENT", "Label", AmountOfMoneyJSON121("EUR", "0"))),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount)
+ )
+
+ apiRelations += ApiRelation(createAccount, createAccount, "self")
+ apiRelations += ApiRelation(createAccount, getCoreAccountById, "detail")
+
+ // Note: This doesn't currently work (links only have access to same version resource docs). TODO fix me.
+ apiRelations += ApiRelation(createAccount, Implementations1_2_1.updateAccountLabel, "update_label")
+
+
+ lazy val createAccount : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ // TODO document this code (make the extract work): "JsonPut json -> _ =>"
+ // Create a new account
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonPut json -> _ => {
+ user => {
+
+ 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) ?~ ErrorMessages.InvalidUserId
+ postedOrLoggedInUser <- User.findByUserId(user_id) ?~! ErrorMessages.UserNotFoundById
+ bank <- Bank(bankId) ?~ s"Bank $bankId not found"
+ // 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
+ accountLabel <- tryo(jsonBody.`type`) //?~ ErrorMessages.InvalidAccountLabel
+ initialBalanceAsNumber <- tryo {BigDecimal(initialBalanceAsString)} ?~! ErrorMessages.InvalidAccountInitalBalance
+ isTrue <- booleanToBox(0 == initialBalanceAsNumber) ?~ s"Initial balance must be zero"
+ 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")
+ bankAccount <- Connector.connector.vend.createSandboxBankAccount(bankId, accountId, accountType, accountLabel, currency, initialBalanceAsNumber, postedOrLoggedInUser.name)
+ } yield {
+ BankAccountCreation.setAsOwner(bankId, accountId, postedOrLoggedInUser)
+
+ val dataContext = DataContext(user, Some(bankAccount.bankId), Some(bankAccount.accountId), Empty, Empty, Empty)
+ val links = code.api.util.APIUtil.getHalLinks(CallerContext(createAccount), codeContext, dataContext)
+ val json = JSONFactory200.createCoreAccountJSON(bankAccount, links)
+
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+
+
+ val getTransactionTypesIsPublic = Props.getBool("apiOptions.getTransactionTypesIsPublic", true)
+
+
+ resourceDocs += ResourceDoc(
+ getTransactionTypes,
+ apiVersion,
+ "getTransactionTypes",
+ "GET",
+ "/banks/BANK_ID/transaction-types",
+ "Get transaction-types offered by the bank",
+ // TODO get the documentation of the parameters from the scala doc of the case class we return
+ s"""Get Transaction Types for the bank specified by BANK_ID:
+ |
+ |Lists the possible Transaction Types available at the bank (as opposed to Transaction Request Types which are the possible ways Transactions can be created by this API Server).
+ |
+ | * id : Unique transaction type id across the API instance. SHOULD be a UUID. MUST be unique.
+ | * bank_id : The bank that supports this TransactionType
+ | * short_code : A short code (SHOULD have no-spaces) which MUST be unique across the bank. May be stored with Transactions to link here
+ | * summary : A succinct summary
+ | * description : A longer description
+ | * charge : The charge to the customer for each one of these
+ |
+ |${authenticationRequiredMessage(!getTransactionTypesIsPublic)}""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ false,
+ List(apiTagBank)
+ )
+
+ lazy val getTransactionTypes : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "transaction-types" :: Nil JsonGet _ => {
+ user => {
+ for {
+ // Get Transaction Types from the active provider
+ u <- if(getTransactionTypesIsPublic)
+ Box(Some(1))
+ else
+ user ?~! "User must be logged in to retrieve Transaction Types data"
+ 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
+ val json = JSONFactory200.createTransactionTypeJSON(transactionTypes)
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+ }
+
+ ///
+
+
+
+ import net.liftweb.json.JsonAST._
+ import net.liftweb.json.Extraction._
+ import net.liftweb.json.Printer._
+ val exchangeRates = pretty(render(decompose(fx.exchangeRates)))
+
+ resourceDocs += ResourceDoc(
+ createTransactionRequest,
+ apiVersion,
+ "createTransactionRequest",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests",
+ "Create Transaction Request.",
+ s"""Initiate a Payment via a Transaction Request.
+ |
+ |This is the preferred method to create a payment and supersedes makePayment in 1.2.1.
+ |
+ |PSD2 Context: Third party access access to payments is a core tenent of PSD2.
+ |
+ |This call satisfies that requirement from several perspectives:
+ |
+ |1) A transaction can be initiated by a third party application.
+ |
+ |2) The customer is informed of the charge that will incurred.
+ |
+ |3) The call uses delegated authentication (OAuth)
+ |
+ |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow.
+ |
+ |In sandbox mode, if the amount is less than 100 (any currency), the transaction request will create a transaction without a challenge, else a challenge will need to be answered.
+ |
+ |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account.
+ |
+ |Currently TRANSACTION_REQUEST_TYPE must be set to SANDBOX_TAN
+ |
+ |The following static FX rates are available in sandbox mode:
+ |
+ |${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_IDO specified in the request body.
+ |
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |""",
+ Extraction.decompose(TransactionRequestBodyJSON (
+ TransactionRequestAccountJSON("BANK_ID", "ACCOUNT_ID"),
+ AmountOfMoneyJSON121("EUR", "100.53"),
+ "A description for the transaction to be created"
+ )
+ ),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
+ TransactionRequestType(transactionRequestType) :: "transaction-requests" :: Nil JsonPost json -> _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ /* TODO:
+ * check if user has access using the view that is given (now it checks if user has access to owner view), will need some new permissions for transaction requests
+ * test: functionality, error messages if user not given or invalid, if any other value is not existing
+ */
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ transBodyJson <- tryo{json.extract[TransactionRequestBodyJSON]} ?~ {ErrorMessages.InvalidJsonFormat}
+ transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)}
+ 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 <- BankAccount(toBankId, toAccountId) ?~! {ErrorMessages.CounterpartyNotFound}
+ // Prevent default value for transaction request type (at least).
+ // Get Transaction Request Types from Props "transactionRequests_supported_types". Default is empty string
+ validTransactionRequestTypes <- tryo{Props.get("transactionRequests_supported_types", "")}
+ // Use a list instead of a string to avoid partial matches
+ validTransactionRequestTypesList <- tryo{validTransactionRequestTypes.split(",")}
+ isValidTransactionRequestType <- tryo(assert(transactionRequestType.value != "TRANSACTION_REQUEST_TYPE" && validTransactionRequestTypesList.contains(transactionRequestType.value))) ?~! s"${ErrorMessages.InvalidTransactionRequestType} : Invalid value is: '${transactionRequestType.value}' Valid values are: ${validTransactionRequestTypes}"
+ transferCurrencyEqual <- tryo(assert(transBodyJson.value.currency == fromAccount.currency)) ?~! {"Transfer body currency and holder account currency must be the same."}
+ createdTransactionRequest <- Connector.connector.vend.createTransactionRequestv200(u, fromAccount, toAccount, transactionRequestType, transBody)
+ } yield {
+ // Explicitly format as v2.0.0 json
+ val json = JSONFactory200.createTransactionRequestWithChargeJSON(createdTransactionRequest)
+ createdJsonResponse(Extraction.decompose(json))
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ answerTransactionRequestChallenge,
+ apiVersion,
+ "answerTransactionRequestChallenge",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests/TRANSACTION_REQUEST_ID/challenge",
+ "Answer Transaction Request Challenge.",
+ "In Sandbox mode, any string that can be converted to a positive integer will be accepted as an answer.",
+ Extraction.decompose(ChallengeAnswerJSON("89123812", "123345")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val answerTransactionRequestChallenge: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
+ TransactionRequestType(transactionRequestType) :: "transaction-requests" :: TransactionRequestId(transReqId) :: "challenge" :: Nil JsonPost json -> _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ 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
+ answerOk <- Connector.connector.vend.answerTransactionRequestChallenge(transReqId, answerJson.answer)
+ //create transaction and insert its id into the transaction request
+ transactionRequest <- Connector.connector.vend.createTransactionAfterChallengev200(u, transReqId)
+ } yield {
+
+ // Format explicitly as v2.0.0 json
+ val json = JSONFactory200.createTransactionRequestWithChargeJSON(transactionRequest)
+ //successJsonResponse(Extraction.decompose(json))
+
+ val successJson = Extraction.decompose(json)
+ successJsonResponse(successJson, 202)
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ getTransactionRequests,
+ apiVersion,
+ "getTransactionRequests",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-requests",
+ "Get Transaction Requests." ,
+ """Returns transaction requests for account specified by ACCOUNT_ID at bank specified by BANK_ID.
+ |
+ |The VIEW_ID specified must be 'owner' and the user must have access to this view.
+ |
+ |Version 2.0.0 now returns charge information.
+ |
+ |Transaction Requests serve to initiate transactions that may or may not proceed. They contain information including:
+ |
+ |* Transaction Request Id
+ |* Type
+ |* Status (INITIATED, COMPLETED)
+ |* Challenge (in order to confirm the request)
+ |* From Bank / Account
+ |* Body including To Account, Currency, Value, Description and other initiation information. (Could potentialy include a list of future transactions.)
+ |* Related Transactions
+ |
+ |PSD2 Context: PSD2 requires transparency of charges to the customer.
+ |This endpoint provides the charge that would be applied if the Transaction Request proceeds - and a record of that charge there after.
+ |The customer can proceed with the Transaction by answering the security challenge.
+ |
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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)
+ }
+ yield {
+ // Format the data as V2.0.0 json
+ val json = JSONFactory200.createTransactionRequestJSONs(transactionRequests)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+
+ 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.
+ |
+ | May require validation of email address.
+ |
+ |""",
+ Extraction.decompose(CreateUserJSON("someone@example.com", "my-username", "my-secure-password", "James", "Brown")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagOnboarding, apiTagUser))
+
+ 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.username, postedData.username)).isEmpty) {
+ val userCreated = OBPUser.create
+ .firstName(postedData.first_name)
+ .lastName(postedData.last_name)
+ .username(postedData.username)
+ .email(postedData.email)
+ .password(postedData.password)
+ .validated(true) // TODO Get this from Props
+ if(userCreated.validate.size > 0){
+ Full(errorJsonResponse(userCreated.validate.map(_.msg).mkString(";")))
+ }
+ else
+ {
+ 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 username already exists.", 409))
+ }
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ createMeeting,
+ apiVersion,
+ "createMeeting",
+ "POST",
+ "/banks/BANK_ID/meetings",
+ "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).
+ |
+ |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,
+ false, // Core
+ false, // PSD2
+ false, // OBWG
+ List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental))
+
+
+ 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(ErrorMessages.MeetingApiKeyNotConfigured, 403)
+ providerSecret <- Props.get("meeting.tokbox_api_secret") ~> APIFailure(ErrorMessages.MeetingApiSecretNotConfigured, 403)
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound}
+ postedData <- tryo {json.extract[CreateMeetingJSON]} ?~ ErrorMessages.InvalidJsonFormat
+ now = Calendar.getInstance().getTime()
+ 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)
+ successJsonResponse(Extraction.decompose(json), 201)
+ }
+ } else {
+ 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,
+ false,
+ false, // PSD2
+ false,
+ List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental))
+
+
+ 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 <- 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}
+ // 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))
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ getMeeting,
+ apiVersion,
+ "getMeeting",
+ "GET",
+ "/banks/BANK_ID/meetings/MEETING_ID",
+ "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.
+ |
+ |Login is required.
+ |
+ |This call is **experimental** and will require further authorisation in the future.
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagMeeting, apiTagKyc, apiTagCustomer, apiTagUser, apiTagExperimental))
+
+
+ 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 <- 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)
+ 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
+ val json = JSONFactory200.createMeetingJSON(meeting)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ } else {
+ Full(errorJsonResponse(ErrorMessages.MeetingsNotSupported))
+ }
+ }
+ }
+
+ //
+
+
+
+ resourceDocs += ResourceDoc(
+ createCustomer,
+ apiVersion,
+ "createCustomer",
+ "POST",
+ "/banks/BANK_ID/customers",
+ "Create Customer.",
+ 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
+ |${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),
+ exampleDate, "Single", 1, List(exampleDate), "Bachelor’s Degree", "Employed", true, exampleDate)),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, 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 =>
+ 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}
+ postedData <- tryo{json.extract[CreateCustomerJson]} ?~! ErrorMessages.InvalidJsonFormat
+ 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
+ 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)) ?~ 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,
+ 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"
+ 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 json = JSONFactory1_4_0.createCustomerJson(customer)
+ val successJson = Extraction.decompose(json)
+ successJsonResponse(successJson, 201)
+ }
+ }
+ }
+
+
+
+ resourceDocs += ResourceDoc(
+ getCurrentUser,
+ apiVersion,
+ "getCurrentUser", // TODO can we get this string from the val two lines above?
+ "GET",
+ "/users/current",
+ "Get User (Current)",
+ """Get the logged in user
+ |
+ |Login is required.
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagPerson, 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))
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ getUser,
+ apiVersion,
+ "getUser",
+ "GET",
+ "/users/USER_EMAIL",
+ "Get Users by Email Address",
+ """Get users by email address
+ |
+ |Login is required.
+ |CanGetAnyUser entitlement is required,
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagPerson, apiTagUser))
+
+
+ lazy val getUser: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "users" :: userEmail :: Nil JsonGet _ => {
+ user =>
+ for {
+ l <- user ?~ ErrorMessages.UserNotLoggedIn
+ canGetAnyUser <- booleanToBox(hasEntitlement("", l.userId, ApiRole.CanGetAnyUser), "CanGetAnyUser entitlement required")
+ // Workaround to get userEmail address directly from URI without needing to URL-encode it
+ users <- tryo{OBPUser.getApiUsersByEmail(CurrentReq.value.uri.split("/").last)} ?~! {ErrorMessages.UserNotFoundByEmail}
+ }
+ yield {
+ // Format the data as V2.0.0 json
+ val json = JSONFactory200.createUserJSONs(users)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ createUserCustomerLinks,
+ apiVersion,
+ "createUserCustomerLinks",
+ "POST",
+ "/banks/user_customer_links",
+ "Create user customer link.",
+ 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.
+ |${authenticationRequiredMessage(true)}
+ |""",
+ Extraction.decompose(CreateUserCustomerLinkJSON("be106783-b4fa-48e6-b102-b178a11a8e9b", "02141bc6-0a69-4fba-b4db-a17e5fbbbdcc")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagPerson, 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 =>
+ for {
+ 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.userCustomerLink.vend.getUserCustomerLink(postedData.user_id, postedData.customer_id).isEmpty == true) ?~ ErrorMessages.CustomerAlreadyExistsForUser
+ userCustomerLink <- UserCustomerLink.userCustomerLink.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)
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ addEntitlement,
+ apiVersion,
+ "addEntitlement",
+ "POST",
+ "/users/USER_ID/entitlements",
+ "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)
+ |
+ |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")),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagUser))
+
+ 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 =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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"
+ isBankOrSystemRoleOk <- booleanToBox(ApiRole.valueOf(postedData.role_name).requiresBankId == postedData.bank_id.nonEmpty) ?~!
+ {if (ApiRole.valueOf(postedData.role_name).requiresBankId) ErrorMessages.EntitlementIsBankRole else ErrorMessages.EntitlementIsSystemRole}
+ 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)
+ } yield {
+ val viewJson = JSONFactory200.createEntitlementJSON(addedEntitlement)
+ successJsonResponse(Extraction.decompose(viewJson), 201)
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ getEntitlements,
+ apiVersion,
+ "getEntitlements",
+ "GET",
+ "/users/USER_ID/entitlements",
+ "Get Entitlements specified by USER_ID",
+ """
+ |
+ |Login is required.
+ |
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagUser, apiTagEntitlement))
+
+
+ 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 <- Entitlement.entitlement.vend.getEntitlements(userId)
+ }
+ yield {
+ var json = EntitlementJSONs(Nil)
+ // Format the data as V2.0.0 json
+ if (isSuperAdmin(u.userId)) {
+ // If the user is SuperAdmin add it to the list
+ json = EntitlementJSONs(JSONFactory200.createEntitlementJSONs(entitlements).list:::List(EntitlementJSON("", "SuperAdmin", "")))
+ successJsonResponse(Extraction.decompose(json))
+ } else {
+ json = JSONFactory200.createEntitlementJSONs(entitlements)
+ }
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ deleteEntitlement,
+ apiVersion,
+ "deleteEntitlement",
+ "DELETE",
+ "/users/USER_ID/entitlement/ENTITLEMENT_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.
+ |
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagUser, apiTagEntitlement))
+
+
+ lazy val deleteEntitlement: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "users" :: userId :: "entitlement" :: entitlementId :: Nil JsonDelete _ => {
+ user =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ isSuperAdmin <- booleanToBox(isSuperAdmin(u.userId)) ?~ "User is not super admin!"
+ entitlement <- tryo{Entitlement.entitlement.vend.getEntitlement(entitlementId)} ?~ "EntitlementId not found"
+ deleted <- Entitlement.entitlement.vend.deleteEntitlement(entitlement)
+ }
+ yield noContentJsonResponse
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ getAllEntitlements,
+ apiVersion,
+ "getAllEntitlements",
+ "GET",
+ "/entitlements",
+ "Get all Entitlements",
+ """
+ |
+ |Login is required.
+ |
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagUser, apiTagEntitlement))
+
+
+ lazy val getAllEntitlements: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "entitlements" :: Nil JsonGet _ => {
+ user =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ isSuperAdmin <- booleanToBox(isSuperAdmin(u.userId)) ?~ "Logged user is not super admin!"
+ entitlements <- Entitlement.entitlement.vend.getEntitlements
+ }
+ yield {
+ // Format the data as V2.0.0 json
+ val json = JSONFactory200.createEntitlementJSONs(entitlements)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+
+ // TODO Put message into doc below if not enabled (but continue to show API Doc)
+ resourceDocs += ResourceDoc(
+ elasticSearchWarehouse,
+ apiVersion,
+ "elasticSearchWarehouse",
+ "GET",
+ "/search/warehouse",
+ "Search Warehouse Data Via Elasticsearch",
+ """
+ |Search warehouse data via Elastic Search.
+ |
+ |Login is required.
+ |
+ |CanSearchWarehouse entitlement is required to search warehouse data!
+ |
+ |Send your email, name, project name and user_id to the admins to get access.
+ |
+ |Elastic (search) is used in the background. See links below for syntax.
+ |
+ |
+ |parameters:
+ |
+ | esType - elasticsearch type
+ |
+ | simple query:
+ |
+ | q - plain_text_query
+ |
+ | df - default field to search
+ |
+ | sort - field to sort on
+ |
+ | size - number of hits returned, default 10
+ |
+ | from - show hits starting from
+ |
+ | json query:
+ |
+ | source - JSON_query_(URL-escaped)
+ |
+ |
+ |Example usage:
+ |
+ |GET /search/warehouse/q=findThis
+ |
+ |or:
+ |
+ |GET /search/warehouse/source={"query":{"query_string":{"query":"findThis"}}}
+ |
+ |
+ |Note!!
+ |
+ |The whole JSON query string MUST be URL-encoded:
+ |
+ |* For { use %7B
+ |* For } use %7D
+ |* For : use %3A
+ |* For " use %22
+ |
+ |etc..
+ |
+ |
+ |
+ |Only q, source and esType are passed to Elastic
+ |
+ |Elastic simple query: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html
+ |
+ |Elastic JSON query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html
+ |
+ |You can specify the esType thus: /search/warehouse/esType=type&q=a
+ |
+ """,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List())
+
+ val esw = new elasticsearchWarehouse
+ lazy val elasticSearchWarehouse: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "search" :: "warehouse" :: queryString :: Nil JsonGet _ => {
+ user =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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)))
+ }
+ }
+ }
+
+ // TODO Put message into doc below if not enabled (but continue to show API Doc)
+ resourceDocs += ResourceDoc(
+ elasticSearchMetrics,
+ apiVersion,
+ "elasticSearchMetrics",
+ "GET",
+ "/search/metrics",
+ "Search API Metrics via Elasticsearch.",
+ """
+ |Search the API calls made to this API instance via Elastic Search.
+ |
+ |Login is required.
+ |
+ |CanSearchMetrics entitlement is required to search metrics data.
+ |
+ |
+ |parameters:
+ |
+ | esType - elasticsearch type
+ |
+ | simple query:
+ |
+ | q - plain_text_query
+ |
+ | df - default field to search
+ |
+ | sort - field to sort on
+ |
+ | size - number of hits returned, default 10
+ |
+ | from - show hits starting from
+ |
+ | json query:
+ |
+ | source - JSON_query_(URL-escaped)
+ |
+ |
+ |example usage:
+ |
+ | /search/metrics/q=findThis
+ |
+ |or:
+ |
+ | /search/metrics/source={"query":{"query_string":{"query":"findThis"}}}
+ |
+ |
+ |Note!!
+ |
+ |The whole JSON query string MUST be URL-encoded:
+ |
+ |* For { use %7B
+ |* For } use %7D
+ |* For : use %3A
+ |* For " use %22
+ |
+ |etc..
+ |
+ |
+ |
+ |Only q, source and esType are passed to Elastic
+ |
+ |Elastic simple query: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html
+ |
+ |Elastic JSON query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html
+ |
+ |
+ """,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List())
+
+ val esm = new elasticsearchMetrics
+ lazy val elasticSearchMetrics: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "search" :: "metrics" :: queryString :: Nil JsonGet _ => {
+ user =>
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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)))
+ }
+ }
+ }
+
+
+ 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(apiTagPerson, 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))
+ }
+ }
+ }
+ }
+
+
+
+
+
+
+
+
+
+ }
+
+
+}
+
+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..c87b11b65
--- /dev/null
+++ b/src/main/scala/code/api/v2_0_0/JSONFactory2.0.0.scala
@@ -0,0 +1,814 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2015, 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.
+
+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.net.URL
+import java.util.Date
+
+import code.TransactionTypes.TransactionType.TransactionType
+import code.entitlement.Entitlement
+import code.meetings.Meeting
+import code.model.dataAccess.OBPUser
+import code.transactionrequests.TransactionRequests._
+import net.liftweb.common.{Box, Full}
+
+// 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, CustomerFaceImageJson, TransactionRequestAccountJSON}
+import code.kycchecks.KycCheck
+import code.kycdocuments.KycDocument
+import code.kycmedias.KycMedia
+import code.kycstatuses.KycStatus
+import code.model._
+import code.socialmedia.SocialMedia
+import net.liftweb.json.JsonAST.JValue
+
+
+
+// New in 2.0.0
+
+class LinkJSON(
+ val href: URL,
+ val rel: String,
+ val method: String
+)
+
+class LinksJSON(
+ val _links: List[LinkJSON]
+)
+
+class ResultAndLinksJSON(
+ val result : JValue,
+ val links: LinksJSON
+)
+
+case class CreateUserJSON(
+ email: String,
+ username: String,
+ password: String,
+ first_name: String,
+ last_name: String
+ )
+
+case class CreateUserJSONs(
+ users : List[CreateUserJSON]
+ )
+
+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 MeetingJSONs(
+ meetings : List[MeetingJSON]
+ )
+
+
+case class MeetingKeysJSON(
+ session_id: String,
+ staff_token: String,
+ customer_token: String
+ )
+
+case class MeetingPresentJSON(
+ staff_user_id: String,
+ customer_user_id: String
+
+ )
+
+case class UserCustomerLinkJSON(user_customer_link_id: String,
+ customer_id: String,
+ user_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,
+ val is_public: Boolean
+)
+
+case class BasicAccountsJSON(
+ accounts : List[BasicAccountJSON]
+)
+
+// Basic Account has basic View
+case class BasicAccountJSON(
+ id : String,
+ label : String,
+ views_available : List[BasicViewJSON],
+ bank_id : String
+)
+
+
+// Json used in account creation
+case class CreateAccountJSON(
+ user_id : String,
+ label : String,
+ `type` : String,
+ balance : AmountOfMoneyJSON
+ )
+
+// No view in core
+case class CoreAccountJSON(
+ id : String,
+ label : String,
+ bank_id : String,
+ _links: JValue
+ )
+
+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])
+
+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
+
+
+/** A TransactionType categorises a transaction on a bank statement.
+ *
+ * i.e. it justifies the reason for a transaction on a bank statement to exist
+ * e.g. a bill-payment, ATM-withdrawal, interest-payment or some kind of fee to the customer.
+ *
+ * This is the JSON respresentation (v2.0.0) of the object
+ *
+ * @param id Unique id across the API instance. Ideally a UUID
+ * @param bank_id The bank that supports this TransactionType
+ * @param short_code A short code (ideally-no-spaces) which is unique across the bank. Should map to transaction.details.types
+ * @param summary A succinct summary
+ * @param description A longer description
+ * @param charge The fee to the customer for each one of these
+ */
+
+case class TransactionTypeJSON (
+ id: TransactionTypeId,
+ bank_id : String,
+ short_code : String,
+ summary: String,
+ description: String,
+ charge: AmountOfMoneyJSON
+ )
+
+
+
+case class TransactionTypesJSON(transaction_types: List[TransactionTypeJSON])
+
+
+
+/*
+v2.0.0 Json Representation of TransactionRequest
+ */
+
+
+case class TransactionRequestChargeJSON(
+ val summary: String,
+ val value : AmountOfMoneyJSON
+ )
+
+
+case class TransactionRequestJSON(
+ id: String,
+ `type`: String,
+ from: TransactionRequestAccountJSON,
+ body: TransactionRequestBodyJSON,
+ transaction_ids: String,
+ status: String,
+ start_date: Date,
+ end_date: Date,
+ challenge: ChallengeJSON
+ )
+
+
+case class TransactionRequestWithChargeJSON(
+ id: String,
+ `type`: String,
+ from: TransactionRequestAccountJSON,
+ body: TransactionRequestBodyJSON,
+ transaction_ids: String,
+ status: String,
+ start_date: Date,
+ end_date: Date,
+ challenge: ChallengeJSON,
+ charge : TransactionRequestChargeJSON
+ )
+
+
+
+
+
+case class TransactionRequestWithChargeJSONs(
+ transaction_requests_with_charges : List[TransactionRequestWithChargeJSON]
+ )
+
+
+
+
+
+
+
+case class TransactionRequestBodyJSON (
+ to: TransactionRequestAccountJSON,
+ value : AmountOfMoneyJSON,
+ description : String
+ )
+
+case class CreateEntitlementJSON(bank_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{
+
+ // Modified in 2.0.0
+
+ //transaction requests
+ def getTransactionRequestBodyFromJson(body: TransactionRequestBodyJSON) : TransactionRequestBody = {
+ val toAcc = TransactionRequestAccount (
+ bank_id = body.to.bank_id,
+ account_id = body.to.account_id
+ )
+ val amount = AmountOfMoney (
+ currency = body.value.currency,
+ amount = body.value.amount
+ )
+
+ TransactionRequestBody (
+ to = toAcc,
+ value = amount,
+ description = body.description
+ )
+ }
+
+ def getTransactionRequestFromJson(json : TransactionRequestJSON) : TransactionRequest = {
+ val fromAcc = TransactionRequestAccount (
+ json.from.bank_id,
+ json.from.account_id
+ )
+ val challenge = TransactionRequestChallenge (
+ id = json.challenge.id,
+ allowed_attempts = json.challenge.allowed_attempts,
+ challenge_type = json.challenge.challenge_type
+ )
+
+ val charge = TransactionRequestCharge("Total charges for a completed transaction request.", AmountOfMoney(json.body.value.currency, "0.05"))
+
+
+ TransactionRequest (
+ id = TransactionRequestId(json.id),
+ `type`= json.`type`,
+ from = fromAcc,
+ body = getTransactionRequestBodyFromJson(json.body),
+ transaction_ids = json.transaction_ids,
+ status = json.status,
+ start_date = json.start_date,
+ end_date = json.end_date,
+ challenge = challenge,
+ charge = charge
+ )
+ }
+
+
+
+
+
+
+
+
+
+ // New in 2.0.0
+
+
+ def createViewBasicJSON(view : View) : BasicViewJSON = {
+ val alias =
+ if(view.usePublicAliasIfOneExists)
+ "public"
+ else if(view.usePrivateAliasIfOneExists)
+ "private"
+ else
+ ""
+
+ new BasicViewJSON(
+ id = view.viewId.value,
+ short_name = stringOrNull(view.name),
+ is_public = view.isPublic
+ )
+ }
+
+
+ def createBasicAccountJSON(account : BankAccount, basicViewsAvailable : List[BasicViewJSON] ) : BasicAccountJSON = {
+ new BasicAccountJSON(
+ account.accountId.value,
+ stringOrNull(account.label),
+ basicViewsAvailable,
+ account.bankId.value
+ )
+ }
+
+ // Contains only minimal info (could have more if owner) plus links
+ def createCoreAccountJSON(account : BankAccount, links: JValue ) : CoreAccountJSON = {
+ val coreAccountJson = new CoreAccountJSON(
+ account.accountId.value,
+ stringOrNull(account.label),
+ account.bankId.value,
+ links
+ )
+ coreAccountJson
+ }
+
+
+ case class ModeratedCoreAccountJSON(
+ id : String,
+ label : String,
+ number : String,
+ owners : List[UserJSON121],
+ `type` : String,
+ balance : AmountOfMoneyJSON,
+ 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
+ )
+
+
+
+ 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 : AmountOfMoneyJSON,
+ value : AmountOfMoneyJSON
+ )
+
+
+
+ //
+ case class UserJSON(
+ user_id: String,
+ email : String,
+ provider_id: String,
+ provider : String,
+ username : String
+ )
+
+ case class UserJSONs(
+ users: List[UserJSON]
+ )
+
+
+ def createUserJSONfromOBPUser(user : OBPUser) : UserJSON = new UserJSON(
+ user_id = user.user.foreign.get.userId,
+ email = user.email,
+ username = stringOrNull(user.username),
+ provider_id = stringOrNull(user.provider),
+ provider = stringOrNull(user.provider)
+ )
+
+
+ def createUserJSON(user : User) : UserJSON = {
+ new UserJSON(
+ user_id = user.userId,
+ email = user.emailAddress,
+ username = stringOrNull(user.name),
+ provider_id = user.idGivenByProvider,
+ provider = stringOrNull(user.provider)
+ )
+ }
+
+ def createUserJSON(user : Box[User]) : UserJSON = {
+ user match {
+ case Full(u) => createUserJSON(u)
+ case _ => null
+ }
+ }
+
+ def createUserJSONs(users : List[User]) : UserJSONs = {
+ UserJSONs(users.map(createUserJSON))
+ }
+
+
+
+ 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),
+ 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,
+ 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,
+ 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))
+ }
+
+
+ /** Creates v2.0.0 representation of a TransactionType
+ *
+ * @param transactionType An internal TransactionType instance
+ * @return a v2.0.0 representation of a TransactionType
+ */
+
+def createTransactionTypeJSON(transactionType : TransactionType) : TransactionTypeJSON = {
+ new TransactionTypeJSON(
+ id = transactionType.id,
+ bank_id = transactionType.bankId.toString,
+ short_code = transactionType.shortCode,
+ summary = transactionType.summary,
+ description = transactionType.description,
+ charge = new AmountOfMoneyJSON(currency = transactionType.charge.currency, amount = transactionType.charge.amount)
+ )
+ }
+ def createTransactionTypeJSON(transactionTypes : List[TransactionType]) : TransactionTypesJSON = {
+ TransactionTypesJSON(transactionTypes.map(createTransactionTypeJSON))
+ }
+
+
+
+
+ /** Creates v2.0.0 representation of a TransactionType
+ *
+ * @param tr An internal TransactionRequest instance
+ * @return a v2.0.0 representation of a TransactionRequest
+ */
+
+ def createTransactionRequestWithChargeJSON(tr : TransactionRequest) : TransactionRequestWithChargeJSON = {
+ new TransactionRequestWithChargeJSON(
+ id = tr.id.value,
+ `type` = tr.`type`,
+ from = TransactionRequestAccountJSON (
+ bank_id = tr.from.bank_id,
+ account_id = tr.from.account_id),
+ body = TransactionRequestBodyJSON (
+ to = TransactionRequestAccountJSON (
+ bank_id = tr.body.to.bank_id,
+ account_id = tr.body.to.account_id),
+ value = AmountOfMoneyJSON (currency = tr.body.value.currency, amount = tr.body.value.amount),
+ description = tr.body.description),
+ transaction_ids = tr.transaction_ids,
+ status = tr.status,
+ start_date = tr.start_date,
+ end_date = tr.end_date,
+ // Some (mapped) data might not have the challenge. TODO Make this nicer
+ challenge = {
+ try {ChallengeJSON (id = tr.challenge.id, allowed_attempts = tr.challenge.allowed_attempts, challenge_type = tr.challenge.challenge_type)}
+ // catch { case _ : Throwable => ChallengeJSON (id = "", allowed_attempts = 0, challenge_type = "")}
+ catch { case _ : Throwable => null}
+ },
+ charge = TransactionRequestChargeJSON (summary = tr.charge.summary,
+ value = AmountOfMoneyJSON(currency = tr.charge.value.currency,
+ amount = tr.charge.value.amount)
+ )
+ )
+ }
+ def createTransactionRequestJSONs(trs : List[TransactionRequest]) : TransactionRequestWithChargeJSONs = {
+ 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)
+
+ }
+
+ def createMeetingJSONs(meetings : List[Meeting]) : MeetingJSONs = {
+ MeetingJSONs(meetings.map(createMeetingJSON))
+ }
+
+ def createUserCustomerLinkJSON(ucl: code.usercustomerlinks.UserCustomerLink) = {
+ UserCustomerLinkJSON(user_customer_link_id = ucl.userCustomerLinkId,
+ customer_id = ucl.customerId,
+ user_id = ucl.userId,
+ date_inserted = ucl.dateInserted,
+ is_active = ucl.isActive
+ )
+ }
+
+ def createUserCustomerLinkJSONs(ucls: List[code.usercustomerlinks.UserCustomerLink]): UserCustomerLinkJSONs = {
+ UserCustomerLinkJSONs(ucls.map(createUserCustomerLinkJSON))
+ }
+
+ def createEntitlementJSON(e: Entitlement): EntitlementJSON = {
+ EntitlementJSON(entitlement_id = e.entitlementId,
+ role_name = e.roleName,
+ bank_id = e.bankId)
+ }
+
+ 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)
+ 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
new file mode 100644
index 000000000..edd428ad7
--- /dev/null
+++ b/src/main/scala/code/api/v2_0_0/OBPAPI2_0_0.scala
@@ -0,0 +1,191 @@
+/**
+ * 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.OBPRestHelper
+import code.api.v1_3_0.APIMethods130
+import code.api.v1_4_0.APIMethods140
+import net.liftweb.common.Loggable
+
+object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 with APIMethods200 with Loggable {
+
+
+ val VERSION = "2.0.0"
+
+
+ // Note: Since we pattern match on these routes, if two implementations match a given url the first will match
+
+ var routes = List(
+ Implementations1_2_1.root(VERSION),
+ Implementations1_2_1.getBanks,
+ Implementations1_2_1.bankById,
+ // 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,
+ 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,
+ // Now in 2.0.0 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,
+ // Now in 2.0.0 Implementations1_4_0.createTransactionRequest,
+ // Now in 2.0.0 Implementations1_4_0.getTransactionRequests,
+ Implementations1_4_0.getTransactionRequestTypes,
+
+ // Updated in 2.0.0 (less info about the views)
+ Implementations2_0_0.allAccountsAllBanks,
+ Implementations2_0_0.privateAccountsAllBanks,
+ Implementations2_0_0.publicAccountsAllBanks,
+ Implementations2_0_0.allAccountsAtOneBank,
+ Implementations2_0_0.privateAccountsAtOneBank,
+ Implementations2_0_0.publicAccountsAtOneBank,
+ Implementations2_0_0.createTransactionRequest,
+ Implementations2_0_0.answerTransactionRequestChallenge,
+ Implementations2_0_0.getTransactionRequests, // Now has charges information
+ // Updated in 2.0.0 (added sorting and better guards / error messages)
+ Implementations2_0_0.accountById,
+ Implementations2_0_0.getPermissionsForBankAccount,
+ Implementations2_0_0.getPermissionForUserForBankAccount,
+ // New in 2.0.0
+ Implementations2_0_0.getKycDocuments,
+ Implementations2_0_0.getKycMedia,
+ Implementations2_0_0.getKycStatuses,
+ 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.addSocialMediaHandle,
+ Implementations2_0_0.getCoreAccountById,
+ Implementations2_0_0.getCoreTransactionsForBankAccount,
+ Implementations2_0_0.createAccount,
+ Implementations2_0_0.getTransactionTypes,
+ Implementations2_0_0.createUser,
+ Implementations2_0_0.createMeeting,
+ Implementations2_0_0.getMeetings,
+ 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,
+ Implementations2_0_0.deleteEntitlement,
+ Implementations2_0_0.getAllEntitlements,
+ Implementations2_0_0.elasticSearchWarehouse,
+ Implementations2_0_0.elasticSearchMetrics,
+ Implementations2_0_0.getCustomers
+ )
+
+ routes.foreach(route => {
+ oauthServe(apiPrefix{route})
+ })
+
+
+
+
+}
diff --git a/src/main/scala/code/api/v2_1_0/APIMethods210.scala b/src/main/scala/code/api/v2_1_0/APIMethods210.scala
new file mode 100644
index 000000000..b64e0ba89
--- /dev/null
+++ b/src/main/scala/code/api/v2_1_0/APIMethods210.scala
@@ -0,0 +1,357 @@
+package code.api.v2_1_0
+
+import java.text.SimpleDateFormat
+import code.api.util.ApiRole._
+import code.api.util.ErrorMessages
+import code.api.v1_2_1.AmountOfMoneyJSON
+import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJSON
+import code.api.v2_0_0.JSONFactory200._
+import code.api.v2_0_0.{JSONFactory200, TransactionRequestBodyJSON}
+import code.api.v2_1_0.JSONFactory210._
+import code.bankconnectors.Connector
+import code.fx.fx
+import code.model._
+
+import net.liftweb.http.Req
+import net.liftweb.json.Extraction
+import net.liftweb.json.JsonAST.JValue
+import net.liftweb.util.Props
+
+import scala.collection.immutable.Nil
+import scala.collection.mutable.ArrayBuffer
+// Makes JValue assignment to Nil work
+import code.util.Helper._
+import net.liftweb.json.JsonDSL._
+
+import code.api.APIFailure
+import code.api.util.APIUtil._
+import code.sandbox.{OBPDataImport, SandboxDataImport}
+import code.util.Helper
+import net.liftweb.common.{Empty, Full, Box}
+import net.liftweb.http.JsonResponse
+import net.liftweb.http.js.JE.JsRaw
+import net.liftweb.http.rest.RestHelper
+import net.liftweb.util.Helpers._
+import net.liftweb.json._
+import net.liftweb.json.Serialization.{read, write}
+
+
+trait APIMethods210 {
+ //needs to be a RestHelper to get access to JsonGet, JsonPost, etc.
+ self: RestHelper =>
+
+ // helper methods begin here
+ // helper methods end here
+
+ val Implementations2_1_0 = new Object() {
+
+ val resourceDocs = ArrayBuffer[ResourceDoc]()
+ val apiRelations = ArrayBuffer[ApiRelation]()
+
+ val emptyObjectJson: JValue = Nil
+ val apiVersion: String = "2_1_0"
+
+ val exampleDateString: String = "22/08/2013"
+ val simpleDateFormat: SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy")
+ val exampleDate = simpleDateFormat.parse(exampleDateString)
+
+ val codeContext = CodeContext(resourceDocs, apiRelations)
+
+
+ resourceDocs += ResourceDoc(
+ sandboxDataImport,
+ apiVersion,
+ "sandboxDataImport",
+ "POST",
+ "/sandbox/data-import",
+ "Import data into the sandbox.",
+ s"""Import bulk data into the sandbox (Authenticated access).
+ |The user needs to have CanCreateSandbox entitlement.
+ |
+ |An example of an import set of data (json) can be found [here](https://raw.githubusercontent.com/OpenBankProject/OBP-API/develop/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json)
+ |${authenticationRequiredMessage(true)}
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagAccount, apiTagPrivateData, apiTagPublicData))
+
+
+ lazy val sandboxDataImport: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ // Import data into the sandbox
+ case "sandbox" :: "data-import" :: Nil JsonPost json -> _ => {
+ user =>
+ for {
+ u <- user ?~! ErrorMessages.UserNotLoggedIn
+ allowDataImportProp <- Props.get("allow_sandbox_data_import") ~> APIFailure("Data import is disabled for this API instance.", 403)
+ allowDataImport <- Helper.booleanToBox(allowDataImportProp == "true") ~> APIFailure("Data import is disabled for this API instance.", 403)
+ canCreateSandbox <- booleanToBox(hasEntitlement("", u.userId, CanCreateSandbox), s"$CanCreateSandbox entitlement required")
+ importData <- tryo {json.extract[SandboxDataImport]} ?~ "invalid json"
+ importWorked <- OBPDataImport.importer.vend.importData(importData)
+ } yield {
+ successJsonResponse(JsRaw("{}"), 201)
+ }
+ }
+ }
+
+
+ val getTransactionRequestTypesIsPublic = Props.getBool("apiOptions.getTransactionRequestTypesIsPublic", true)
+
+ resourceDocs += ResourceDoc(
+ getTransactionRequestTypesSupportedByBank,
+ apiVersion,
+ "getTransactionRequestTypesSupportedByBank",
+ "GET",
+ "/banks/BANK_ID/transaction-request-types",
+ "Get the Transaction Request Types supported by the bank",
+ s"""Get the list of the Transaction Request Types supported by the bank.
+ |
+ |${authenticationRequiredMessage(!getTransactionRequestTypesIsPublic)}
+ |""",
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ false,
+ false,
+ false,
+ List(apiTagBank, apiTagTransactionRequest))
+
+
+ lazy val getTransactionRequestTypesSupportedByBank: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ // Get transaction request types supported by the bank
+ case "banks" :: BankId(bankId) :: "transaction-request-types" :: Nil JsonGet _ => {
+ user =>
+ for {
+ u <- if(getTransactionRequestTypesIsPublic)
+ Box(Some(1))
+ else
+ user ?~! ErrorMessages.UserNotLoggedIn
+ bank <- Bank(bankId) ?~! {ErrorMessages.BankNotFound}
+ // Get Transaction Request Types from Props "transactionRequests_supported_types". Default is empty string
+ transactionRequestTypes <- tryo(Props.get("transactionRequests_supported_types", ""))
+ } yield {
+ // Format the data as json
+ val json = JSONFactory210.createTransactionRequestTypeJSON(transactionRequestTypes.split(",").toList)
+ // Return
+ successJsonResponse(Extraction.decompose(json))
+ }
+ }
+ }
+
+
+ import net.liftweb.json.JsonAST._
+ import net.liftweb.json.Extraction._
+ import net.liftweb.json.Printer._
+ val exchangeRates = pretty(render(decompose(fx.exchangeRates)))
+
+ resourceDocs += ResourceDoc(
+ createTransactionRequest,
+ apiVersion,
+ "createTransactionRequest",
+ "POST",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests",
+ "Create Transaction Request.",
+ s"""Initiate a Payment via a Transaction Request.
+ |
+ |This is the preferred method to create a payment and supersedes makePayment in 1.2.1.
+ |
+ |PSD2 Context: Third party access access to payments is a core tenent of PSD2.
+ |
+ |This call satisfies that requirement from several perspectives:
+ |
+ |1) A transaction can be initiated by a third party application.
+ |
+ |2) The customer is informed of the charge that will incurred.
+ |
+ |3) The call uses delegated authentication (OAuth)
+ |
+ |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow.
+ |
+ |In sandbox mode, if the amount is less than 100 (any currency), the transaction request will create a transaction without a challenge, else a challenge will need to be answered.
+ |
+ |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account.
+ |
+ |Currently TRANSACTION_REQUEST_TYPE must be set to SANDBOX_TAN
+ |
+ |The following static FX rates are available in sandbox mode:
+ |
+ |${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_IDO specified in the request body.
+ |
+ |
+ |${authenticationRequiredMessage(true)}
+ |
+ |""",
+ Extraction.decompose(TransactionRequestBodyJSON (
+ TransactionRequestAccountJSON("BANK_ID", "ACCOUNT_ID"),
+ AmountOfMoneyJSON("EUR", "100.53"),
+ "A description for the transaction to be created"
+ )
+ ),
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
+ TransactionRequestType(transactionRequestType) :: "transaction-requests" :: Nil JsonPost json -> _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ /* TODO:
+ * check if user has access using the view that is given (now it checks if user has access to owner view), will need some new permissions for transaction requests
+ * test: functionality, error messages if user not given or invalid, if any other value is not existing
+ */
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+
+ // Get Transaction Request Types from Props "transactionRequests_supported_types". Default is empty string
+ validTransactionRequestTypes <- tryo{Props.get("transactionRequests_supported_types", "")}
+ // Use a list instead of a string to avoid partial matches
+ validTransactionRequestTypesList <- tryo{validTransactionRequestTypes.split(",")}
+ isValidTransactionRequestType <- tryo(assert(transactionRequestType.value != "TRANSACTION_REQUEST_TYPE" && validTransactionRequestTypesList.contains(transactionRequestType.value))) ?~! s"${ErrorMessages.InvalidTransactionRequestType} : Invalid value is: '${transactionRequestType.value}' Valid values are: ${validTransactionRequestTypes}"
+
+ transDetailsJson <- transactionRequestType.value match {
+ case "SANDBOX_TAN" => tryo {
+ json.extract[TransactionRequestDetailsSandBoxTanJSON]
+ } ?~ {
+ ErrorMessages.InvalidJsonFormat
+ }
+ case "SEPA" => tryo {
+ json.extract[TransactionRequestDetailsSEPAJSON]
+ } ?~ {
+ ErrorMessages.InvalidJsonFormat
+ }
+ case "FREE_FORM" => tryo {
+ json.extract[TransactionRequestDetailsFreeFormJSON]
+ } ?~ {
+ ErrorMessages.InvalidJsonFormat
+ }
+ }
+
+ transDetails <- transactionRequestType.value match {
+ case "SANDBOX_TAN" => tryo{getTransactionRequestDetailsSandBoxTanFromJson(transDetailsJson.asInstanceOf[TransactionRequestDetailsSandBoxTanJSON])}
+ case "SEPA" => tryo{getTransactionRequestDetailsSEPAFromJson(transDetailsJson.asInstanceOf[TransactionRequestDetailsSEPAJSON])}
+ case "FREE_FORM" => tryo{getTransactionRequestDetailsFreeFormFromJson(transDetailsJson.asInstanceOf[TransactionRequestDetailsFreeFormJSON])}
+ }
+
+ 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)
+
+ // Prevent default value for transaction request type (at least).
+ transferCurrencyEqual <- tryo(assert(transDetailsJson.value.currency == fromAccount.currency)) ?~! {"Transfer body currency and holder account currency must be the same."}
+
+ transDetailsSerialized<- transactionRequestType.value match {
+ case "FREE_FORM" => tryo{
+ implicit val formats = Serialization.formats(NoTypeHints)
+ write(json)
+ }
+ case _ => tryo{
+ implicit val formats = Serialization.formats(NoTypeHints)
+ write(transDetailsJson)
+ }
+ }
+
+ createdTransactionRequest <- transactionRequestType.value match {
+ case "SANDBOX_TAN" => {
+ for {
+ toBankId <- Full(BankId(transDetailsJson.asInstanceOf[TransactionRequestDetailsSandBoxTanJSON].to.bank_id))
+ toAccountId <- Full(AccountId(transDetailsJson.asInstanceOf[TransactionRequestDetailsSandBoxTanJSON].to.account_id))
+ toAccount <- BankAccount(toBankId, toAccountId) ?~! {ErrorMessages.CounterpartyNotFound}
+
+ createdTransactionRequest <- Connector.connector.vend.createTransactionRequestv210(u, fromAccount, Full(toAccount), transactionRequestType, transDetails, transDetailsSerialized)
+ } yield createdTransactionRequest
+
+ }
+ case "SEPA" => {
+ Connector.connector.vend.createTransactionRequestv210(u, fromAccount, Empty, transactionRequestType, transDetails, transDetailsSerialized)
+ }
+ case "FREE_FORM" => {
+ Connector.connector.vend.createTransactionRequestv210(u, fromAccount, Empty, transactionRequestType, transDetails, transDetailsSerialized)
+ }
+ }
+ } yield {
+ // Explicitly format as v2.0.0 json
+ val json = JSONFactory210.createTransactionRequestWithChargeJSON(createdTransactionRequest)
+ createdJsonResponse(Extraction.decompose(json))
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+
+
+ resourceDocs += ResourceDoc(
+ getTransactionRequests,
+ apiVersion,
+ "getTransactionRequests",
+ "GET",
+ "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-requests",
+ "Get Transaction Requests." ,
+ """Returns transaction requests for account specified by ACCOUNT_ID at bank specified by BANK_ID.
+ |
+ |The VIEW_ID specified must be 'owner' and the user must have access to this view.
+ |
+ |Version 2.0.0 now returns charge information.
+ |
+ |Transaction Requests serve to initiate transactions that may or may not proceed. They contain information including:
+ |
+ |* Transaction Request Id
+ |* Type
+ |* Status (INITIATED, COMPLETED)
+ |* Challenge (in order to confirm the request)
+ |* From Bank / Account
+ |* Details including Currency, Value, Description and other initiation information specific to each type. (Could potentialy include a list of future transactions.)
+ |* Related Transactions
+ |
+ |PSD2 Context: PSD2 requires transparency of charges to the customer.
+ |This endpoint provides the charge that would be applied if the Transaction Request proceeds - and a record of that charge there after.
+ |The customer can proceed with the Transaction by answering the security challenge.
+ |
+ |
+ """.stripMargin,
+ emptyObjectJson,
+ emptyObjectJson,
+ emptyObjectJson :: Nil,
+ true,
+ true,
+ true,
+ List(apiTagTransactionRequest))
+
+ lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
+ case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => {
+ user =>
+ if (Props.getBool("transactionRequests_enabled", false)) {
+ for {
+ u <- user ?~ ErrorMessages.UserNotLoggedIn
+ 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.getTransactionRequests210(u, fromAccount)
+ }
+ yield {
+ // Format the data as V2.0.0 json
+ val json = JSONFactory210.createTransactionRequestJSONs(transactionRequests)
+ successJsonResponse(Extraction.decompose(json))
+ }
+ } else {
+ Full(errorJsonResponse("Sorry, Transaction Requests are not enabled in this API instance."))
+ }
+ }
+ }
+ }
+}
+
+object APIMethods210 {
+}
diff --git a/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala b/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala
new file mode 100644
index 000000000..e854a30dd
--- /dev/null
+++ b/src/main/scala/code/api/v2_1_0/JSONFactory2.1.0.scala
@@ -0,0 +1,168 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2015, 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.
+
+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_1_0
+
+import java.util.Date
+
+import code.api.v1_2_1.AmountOfMoneyJSON
+import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeJSON, TransactionRequestAccountJSON}
+import code.api.v2_0_0.{TransactionRequestWithChargeJSONs, TransactionRequestChargeJSON, TransactionRequestBodyJSON}
+import code.model.AmountOfMoney
+import code.transactionrequests.TransactionRequests._
+
+case class TransactionRequestTypeJSON(transaction_request_type: String)
+case class TransactionRequestTypesJSON(transaction_request_types: List[TransactionRequestTypeJSON])
+
+trait TransactionRequestDetailsJSON {
+ val value : AmountOfMoneyJSON
+}
+
+case class TransactionRequestDetailsSandBoxTanJSON(
+ to: TransactionRequestAccountJSON,
+ value : AmountOfMoneyJSON,
+ description : String
+ ) extends TransactionRequestDetailsJSON
+
+case class TransactionRequestDetailsSEPAJSON(
+ value : AmountOfMoneyJSON,
+ IBAN: String,
+ description : String
+ ) extends TransactionRequestDetailsJSON
+
+case class TransactionRequestDetailsFreeFormJSON(
+ value : AmountOfMoneyJSON
+ ) extends TransactionRequestDetailsJSON
+
+case class TransactionRequestWithChargeJSON210(
+ id: String,
+ `type`: String,
+ from: TransactionRequestAccountJSON,
+ details: String,
+ transaction_ids: String,
+ status: String,
+ start_date: Date,
+ end_date: Date,
+ challenge: ChallengeJSON,
+ charge : TransactionRequestChargeJSON
+ )
+
+case class TransactionRequestWithChargeJSONs210(
+ transaction_requests_with_charges : List[TransactionRequestWithChargeJSON210]
+ )
+
+object JSONFactory210{
+ def createTransactionRequestTypeJSON(transactionRequestType : String ) : TransactionRequestTypeJSON = {
+ new TransactionRequestTypeJSON(
+ transactionRequestType
+ )
+ }
+
+ def createTransactionRequestTypeJSON(transactionRequestTypes : List[String]) : TransactionRequestTypesJSON = {
+ TransactionRequestTypesJSON(transactionRequestTypes.map(createTransactionRequestTypeJSON))
+ }
+
+ //transaction requests
+ def getTransactionRequestDetailsSandBoxTanFromJson(details: TransactionRequestDetailsSandBoxTanJSON) : TransactionRequestDetailsSandBoxTan = {
+ val toAcc = TransactionRequestAccount (
+ bank_id = details.to.bank_id,
+ account_id = details.to.account_id
+ )
+ val amount = AmountOfMoney (
+ currency = details.value.currency,
+ amount = details.value.amount
+ )
+
+ TransactionRequestDetailsSandBoxTan (
+ to = toAcc,
+ value = amount,
+ description = details.description
+ )
+ }
+
+ def getTransactionRequestDetailsSEPAFromJson(details: TransactionRequestDetailsSEPAJSON) : TransactionRequestDetailsSEPA = {
+ val amount = AmountOfMoney (
+ currency = details.value.currency,
+ amount = details.value.amount
+ )
+
+ TransactionRequestDetailsSEPA (
+ value = amount,
+ description = details.description
+ )
+ }
+
+ def getTransactionRequestDetailsFreeFormFromJson(details: TransactionRequestDetailsFreeFormJSON) : TransactionRequestDetailsFreeForm = {
+ val amount = AmountOfMoney (
+ currency = details.value.currency,
+ amount = details.value.amount
+ )
+
+ TransactionRequestDetailsFreeForm (
+ value = amount
+ )
+ }
+
+ /** Creates v2.1.0 representation of a TransactionType
+ *
+ * @param tr An internal TransactionRequest instance
+ * @return a v2.1.0 representation of a TransactionRequest
+ */
+
+ def createTransactionRequestWithChargeJSON(tr : TransactionRequest210) : TransactionRequestWithChargeJSON210 = {
+ new TransactionRequestWithChargeJSON210(
+ id = tr.id.value,
+ `type` = tr.`type`,
+ from = TransactionRequestAccountJSON (
+ bank_id = tr.from.bank_id,
+ account_id = tr.from.account_id),
+ details = tr.details,
+ transaction_ids = tr.transaction_ids,
+ status = tr.status,
+ start_date = tr.start_date,
+ end_date = tr.end_date,
+ // Some (mapped) data might not have the challenge. TODO Make this nicer
+ challenge = {
+ try {ChallengeJSON (id = tr.challenge.id, allowed_attempts = tr.challenge.allowed_attempts, challenge_type = tr.challenge.challenge_type)}
+ // catch { case _ : Throwable => ChallengeJSON (id = "", allowed_attempts = 0, challenge_type = "")}
+ catch { case _ : Throwable => null}
+ },
+ charge = TransactionRequestChargeJSON (summary = tr.charge.summary,
+ value = AmountOfMoneyJSON(currency = tr.charge.value.currency,
+ amount = tr.charge.value.amount)
+ )
+ )
+ }
+
+ def createTransactionRequestJSONs(trs : List[TransactionRequest210]) : TransactionRequestWithChargeJSONs210 = {
+ TransactionRequestWithChargeJSONs210(trs.map(createTransactionRequestWithChargeJSON))
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala b/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala
new file mode 100644
index 000000000..ac9f65ae8
--- /dev/null
+++ b/src/main/scala/code/api/v2_1_0/OBPAPI2_1_0.scala
@@ -0,0 +1,197 @@
+/**
+ * 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_1_0
+
+import code.api.OBPRestHelper
+import code.api.v1_3_0.APIMethods130
+import code.api.v1_4_0.APIMethods140
+import code.api.v2_0_0.APIMethods200
+import net.liftweb.common.Loggable
+
+object OBPAPI2_1_0 extends OBPRestHelper with APIMethods130 with APIMethods140 with APIMethods200 with APIMethods210 with Loggable {
+
+
+ val VERSION = "2.1.0"
+
+
+ // Note: Since we pattern match on these routes, if two implementations match a given url the first will match
+
+ var routes = List(
+ Implementations1_2_1.root(VERSION),
+ Implementations1_2_1.getBanks,
+ Implementations1_2_1.bankById,
+ // 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,
+ 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,
+ // Now in 2.0.0 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,
+ // Now in 2.0.0 Implementations1_4_0.createTransactionRequest,
+ // Now in 2.0.0 Implementations1_4_0.getTransactionRequests,
+ Implementations1_4_0.getTransactionRequestTypes,
+
+ // Updated in 2.0.0 (less info about the views)
+ Implementations2_0_0.allAccountsAllBanks,
+ Implementations2_0_0.privateAccountsAllBanks,
+ Implementations2_0_0.publicAccountsAllBanks,
+ Implementations2_0_0.allAccountsAtOneBank,
+ Implementations2_0_0.privateAccountsAtOneBank,
+ Implementations2_0_0.publicAccountsAtOneBank,
+ // Now in 2.1.0 Implementations2_0_0.createTransactionRequest,
+ Implementations2_0_0.answerTransactionRequestChallenge,
+ // Now in 2.1.0 Implementations2_0_0.getTransactionRequests, // Now has charges information
+ // Updated in 2.0.0 (added sorting and better guards / error messages)
+ Implementations2_0_0.accountById,
+ Implementations2_0_0.getPermissionsForBankAccount,
+ Implementations2_0_0.getPermissionForUserForBankAccount,
+ // New in 2.0.0
+ Implementations2_0_0.getKycDocuments,
+ Implementations2_0_0.getKycMedia,
+ Implementations2_0_0.getKycStatuses,
+ 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.addSocialMediaHandle,
+ Implementations2_0_0.getCoreAccountById,
+ Implementations2_0_0.getCoreTransactionsForBankAccount,
+ Implementations2_0_0.createAccount,
+ Implementations2_0_0.getTransactionTypes,
+ Implementations2_0_0.createUser,
+ Implementations2_0_0.createMeeting,
+ Implementations2_0_0.getMeetings,
+ 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,
+ Implementations2_0_0.deleteEntitlement,
+ Implementations2_0_0.getAllEntitlements,
+ Implementations2_0_0.elasticSearchWarehouse,
+ Implementations2_0_0.elasticSearchMetrics,
+ Implementations2_0_0.getCustomers,
+ // New in 2.1.0
+ Implementations2_1_0.sandboxDataImport,
+ Implementations2_1_0.getTransactionRequestTypesSupportedByBank,
+ Implementations2_1_0.createTransactionRequest,
+ Implementations2_1_0.getTransactionRequests
+ )
+
+ routes.foreach(route => {
+ oauthServe(apiPrefix{route})
+ })
+
+
+
+
+}
diff --git a/src/main/scala/code/atms/Atms.scala b/src/main/scala/code/atms/Atms.scala
new file mode 100644
index 000000000..8da87e0d3
--- /dev/null
+++ b/src/main/scala/code/atms/Atms.scala
@@ -0,0 +1,81 @@
+package code.atms
+
+/* For atms */
+
+// Need to import these one by one because in same package!
+
+import code.atms.Atms.{Atm, AtmId}
+import code.model.BankId
+import code.common.{Address, Location, Meta}
+import net.liftweb.common.Logger
+import net.liftweb.util.SimpleInjector
+
+object Atms extends SimpleInjector {
+
+ case class AtmId(value : String)
+
+ trait Atm {
+ def atmId : AtmId
+ def name : String
+ def address : Address
+ def location : Location
+ def meta : Meta
+ }
+
+ val atmsProvider = new Inject(buildOne _) {}
+
+ def buildOne: AtmsProvider = MappedAtmsProvider
+
+ // Helper to get the count out of an option
+ def countOfAtms (listOpt: Option[List[Atm]]) : Int = {
+ val count = listOpt match {
+ case Some(list) => list.size
+ case None => 0
+ }
+ count
+ }
+
+
+}
+
+trait AtmsProvider {
+
+ private val logger = Logger(classOf[AtmsProvider])
+
+
+ /*
+ Common logic for returning atms.
+ */
+ final def getAtms(bankId : BankId) : Option[List[Atm]] = {
+ // If we get atms filter them
+ getAtmsFromProvider(bankId) match {
+ case Some(atms) => {
+
+ val atmsWithLicense = for {
+ branch <- atms if branch.meta.license.name.size > 3 && branch.meta.license.name.size > 3
+ } yield branch
+ Option(atmsWithLicense)
+ }
+ case None => None
+ }
+ }
+
+ /*
+ Return one Atm
+ */
+ final def getAtm(branchId : AtmId) : Option[Atm] = {
+ // Filter out if no license data
+ getAtmFromProvider(branchId).filter(x => x.meta.license.id != "" && x.meta.license.name != "")
+ }
+
+ protected def getAtmFromProvider(branchId : AtmId) : Option[Atm]
+ protected def getAtmsFromProvider(bank : BankId) : Option[List[Atm]]
+
+// End of Trait
+}
+
+
+
+
+
+
diff --git a/src/main/scala/code/atms/MappedAtmsProvider.scala b/src/main/scala/code/atms/MappedAtmsProvider.scala
new file mode 100644
index 000000000..afa505c92
--- /dev/null
+++ b/src/main/scala/code/atms/MappedAtmsProvider.scala
@@ -0,0 +1,81 @@
+package code.atms
+
+import code.atms.Atms._
+import code.common.{Address, License, Location, Meta}
+import code.model.BankId
+import code.util.DefaultStringField
+import net.liftweb.mapper._
+
+object MappedAtmsProvider extends AtmsProvider {
+
+ override protected def getAtmFromProvider(atmId: AtmId): Option[Atm] =
+ MappedAtm.find(By(MappedAtm.mAtmId, atmId.value))
+
+ override protected def getAtmsFromProvider(bankId: BankId): Option[List[Atm]] = {
+ Some(MappedAtm.findAll(By(MappedAtm.mBankId, bankId.value)))
+ }
+
+
+}
+
+class MappedAtm extends Atm with LongKeyedMapper[MappedAtm] with IdPK {
+
+ override def getSingleton = MappedAtm
+
+ object mBankId extends DefaultStringField(this)
+ object mName extends DefaultStringField(this)
+
+ object mAtmId 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)
+
+
+
+ override def atmId: AtmId = AtmId(mAtmId.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 location: Location = new Location {
+ override def latitude: Double = mlocationLatitude
+ override def longitude: Double = mlocationLongitude
+ }
+
+}
+
+//
+object MappedAtm extends MappedAtm with LongKeyedMetaMapper[MappedAtm] {
+ override def dbIndexes = UniqueIndex(mBankId, mAtmId) :: Index(mBankId) :: super.dbIndexes
+}
+
diff --git a/src/main/scala/code/bankbranches/BankBranches.scala b/src/main/scala/code/bankbranches/BankBranches.scala
deleted file mode 100644
index 5041a8cd1..000000000
--- a/src/main/scala/code/bankbranches/BankBranches.scala
+++ /dev/null
@@ -1,60 +0,0 @@
-package code.bankbranches
-
-import code.bankbranches.BankBranches.{BankBranch, DataLicense, BranchData}
-import code.model.BankId
-import net.liftweb.common.Logger
-import net.liftweb.util.SimpleInjector
-
-object BankBranches extends SimpleInjector {
-
- case class BankBranchId(value : String)
- case class BranchData(branches : List[BankBranch], license : DataLicense)
-
- trait DataLicense {
- def name : String
- def url : String
- }
-
- trait BankBranch {
- def branchId : BankBranchId
- def name : String
- def address : Address
- }
-
- trait Address {
- def line1 : String
- def line2 : String
- def line3 : String
- def line4 : String
- def line5 : String
- def postCode : String
- //ISO_3166-1_alpha-2
- def countryCode : String
- }
-
- val bankBranchesProvider = new Inject(buildOne _) {}
-
- def buildOne: BankBranchesProvider = MappedBankBranchesProvider
-
-}
-
-trait BankBranchesProvider {
-
- private val logger = Logger(classOf[BankBranchesProvider])
-
- final def getBranches(bank : BankId) : 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/bankbranches/MappedBankBranchesProvider.scala b/src/main/scala/code/bankbranches/MappedBankBranchesProvider.scala
deleted file mode 100644
index 7af640424..000000000
--- a/src/main/scala/code/bankbranches/MappedBankBranchesProvider.scala
+++ /dev/null
@@ -1,66 +0,0 @@
-package code.bankbranches
-
-import code.bankbranches.BankBranches.{DataLicense, BankBranchId, Address, BankBranch}
-import code.model.BankId
-import code.util.DefaultStringField
-import net.liftweb.mapper._
-
-object MappedBankBranchesProvider extends BankBranchesProvider {
- override protected def branchData(bank: BankId): List[BankBranch] =
- MappedBankBranch.findAll(By(MappedBankBranch.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 {
-
- override def getSingleton = MappedBankBranch
-
- object mBankId extends DefaultStringField(this)
- object mName extends DefaultStringField(this)
-
- object mBranchId extends DefaultStringField(this)
-
- 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)
-
-
- override def branchId: BankBranchId = BankBranchId(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
- }
-}
-
-object MappedBankBranch extends MappedBankBranch with LongKeyedMetaMapper[MappedBankBranch] {
- override def dbIndexes = UniqueIndex(mBankId, mBranchId) :: Index(mBankId) :: super.dbIndexes
-}
-
-class MappedDataLicense extends DataLicense with LongKeyedMapper[MappedDataLicense] with IdPK {
- override def getSingleton = MappedDataLicense
-
- 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 MappedDataLicense extends MappedDataLicense with LongKeyedMetaMapper[MappedDataLicense] {
- override def dbIndexes = Index(mBankId) :: super.dbIndexes
-}
\ No newline at end of file
diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala
index b0b936f3f..4e3bfbe54 100644
--- a/src/main/scala/code/bankconnectors/Connector.scala
+++ b/src/main/scala/code/bankconnectors/Connector.scala
@@ -1,19 +1,61 @@
package code.bankconnectors
-import code.util.Helper._
-import net.liftweb.common.Box
-import code.model._
-import net.liftweb.util.SimpleInjector
-import code.model.User
-import code.model.OtherBankAccount
-import code.model.Transaction
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.transactionrequests.TransactionRequests
+import code.transactionrequests.TransactionRequests._
+import code.util.Helper._
+import net.liftweb.common.{Box, Empty, Failure, Full}
+import net.liftweb.util.Helpers._
+import net.liftweb.util.{Props, SimpleInjector}
+
+import scala.math.BigInt
+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.
+Probably makes sense to have more targeted providers like this.
+
+Could consider a Map of ("resourceType" -> "provider") - this could tell us which tables we need to schemify (for list in Boot), whether or not to
+ initialise MongoDB etc. resourceType might be sub devided to allow for different account types coming from different internal APIs, MQs.
+ */
+
object Connector extends SimpleInjector {
val connector = new Inject(buildOne _) {}
- def buildOne: Connector = LocalMappedConnector
+ def buildOne: Connector = {
+ val connectorProps = Props.get("connector").openOrThrowException("no connector set")
+
+ connectorProps match {
+ case "mapped" => LocalMappedConnector
+ case "mongodb" => LocalConnector
+ case "kafka" => KafkaMappedConnector
+ }
+//
+// if (connectorProps.startsWith("kafka_lib")) {
+// KafkaLibMappedConnector
+// } else {
+// connectorProps match {
+// case "mapped" => LocalMappedConnector
+// case "mongodb" => LocalConnector
+// case "kafka" => KafkaMappedConnector
+// }
+// }
+ }
}
@@ -47,10 +89,14 @@ trait Connector {
//gets banks handled by this connector
def getBanks : List[Bank]
- def getBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] =
- getBankAccountType(bankId, accountId)
+ def getBankAccounts(accounts: List[(BankId, AccountId)]) : List[BankAccount] = {
+ for {
+ acc <- accounts
+ a <- getBankAccount(acc._1, acc._2)
+ } yield a
+ }
- protected def getBankAccountType(bankId : BankId, accountId : AccountId) : Box[AccountType]
+ def getBankAccount(bankId : BankId, accountId : AccountId) : Box[AccountType]
def getOtherBankAccount(bankId: BankId, accountID : AccountId, otherAccountID : String) : Box[OtherBankAccount]
@@ -63,7 +109,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]
@@ -78,21 +124,470 @@ trait Connector {
* @param amt The amount of money to send ( > 0 )
* @return The id of the sender's new transaction,
*/
- def makePayment(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID, amt : BigDecimal) : Box[TransactionId] = {
+ def makePayment(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID,
+ amt : BigDecimal, description : String) : Box[TransactionId] = {
for{
- fromAccount <- getBankAccountType(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 <- getBankAccountType(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}"
})
isPositiveAmtToSend <- booleanToBox(amt > BigDecimal("0"), s"Can't send a payment with a value of 0 or less. ($amt)")
//TODO: verify the amount fits with the currency -> e.g. 12.543 EUR not allowed, 10.00 JPY not allowed, 12.53 EUR allowed
- transactionId <- makePaymentImpl(fromAccount, toAccount, amt)
+ transactionId <- makePaymentImpl(fromAccount, toAccount, amt, description)
} yield transactionId
}
- protected def makePaymentImpl(fromAccount : AccountType, toAccount : AccountType, amt : BigDecimal) : Box[TransactionId]
-}
\ No newline at end of file
+ /**
+ * \
+ *
+ * @param initiator The user attempting to make the payment
+ * @param fromAccountUID The unique identifier of the account sending money
+ * @param toAccountUID The unique identifier of the account receiving money
+ * @param amt The amount of money to send ( > 0 )
+ * @return The id of the sender's new transaction,
+ */
+ def makePaymentv200(initiator : User, fromAccountUID : BankAccountUID, toAccountUID : BankAccountUID,
+ amt : BigDecimal, description : String) : Box[TransactionId] = {
+ for {
+ 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 <- 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}"
+ //})
+
+ // Note: These are guards. Values are calculated in makePaymentImpl
+ rate <- tryo {
+ fx.exchangeRate(fromAccount.currency, toAccount.currency)
+ } ?~! {
+ s"The requested currency conversion (${fromAccount.currency} to ${toAccount.currency}) is not supported."
+ }
+ notUsedHereConvertedAmount <- tryo {
+ fx.convert(amt, rate)
+ } ?~! {
+ "Currency conversion failed."
+ }
+ isPositiveAmtToSend <- booleanToBox(amt > BigDecimal("0"), s"Can't send a payment with a value of 0 or less. ($amt)")
+ //TODO: verify the amount fits with the currency -> e.g. 12.543 EUR not allowed, 10.00 JPY not allowed, 12.53 EUR allowed
+
+
+ transactionId <- makePaymentImpl(fromAccount, toAccount, amt, description)
+ } yield transactionId
+ }
+
+
+ protected def makePaymentImpl(fromAccount : AccountType, toAccount : AccountType, amt : BigDecimal, description : String) : Box[TransactionId]
+
+
+ /*
+ Transaction Requests
+ */
+
+
+ // This is used for 1.4.0 See createTransactionRequestv200 for 2.0.0
+ def createTransactionRequest(initiator : User, fromAccount : BankAccount, toAccount: BankAccount, transactionRequestType: TransactionRequestType, body: TransactionRequestBody) : Box[TransactionRequest] = {
+ //set initial status
+ //for sandbox / testing: depending on amount, we ask for challenge or not
+ val status =
+ if (transactionRequestType.value == TransactionRequests.CHALLENGE_SANDBOX_TAN && BigDecimal(body.value.amount) < 100) {
+ TransactionRequests.STATUS_COMPLETED
+ } else {
+ TransactionRequests.STATUS_INITIATED
+ }
+
+
+
+ //create a new transaction request
+ var result = for {
+ 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 <- 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, {
+ s"Cannot send payment to account with different currency (From ${fromAccount.currency} to ${toAccount.currency}"
+ })
+ isPositiveAmtToSend <- booleanToBox(rawAmt > BigDecimal("0"), s"Can't send a payment with a value of 0 or less. (${rawAmt})")
+ // Version 200 below has more support for charge
+ charge = TransactionRequestCharge("Charge for completed transaction", AmountOfMoney(body.value.currency, "0.00"))
+ transactionRequest <- createTransactionRequestImpl(TransactionRequestId(java.util.UUID.randomUUID().toString), transactionRequestType, fromAccount, toAccount, body, status, charge)
+ } yield transactionRequest
+
+ //make sure we get something back
+ result = Full(result.openOrThrowException("Exception: Couldn't create transactionRequest"))
+
+ //if no challenge necessary, create transaction immediately and put in data store and object to return
+ if (status == TransactionRequests.STATUS_COMPLETED) {
+ val createdTransactionId = Connector.connector.vend.makePayment(initiator, BankAccountUID(fromAccount.bankId, fromAccount.accountId),
+ BankAccountUID(toAccount.bankId, toAccount.accountId), BigDecimal(body.value.amount), body.description)
+
+ //set challenge to null
+ result = Full(result.get.copy(challenge = null))
+
+ //save transaction_id if we have one
+ createdTransactionId match {
+ case Full(ti) => {
+ if (! createdTransactionId.isEmpty) {
+ saveTransactionRequestTransaction(result.get.id, ti)
+ result = Full(result.get.copy(transaction_ids = ti.value))
+ }
+ }
+ case _ => None
+ }
+ } else {
+ //if challenge necessary, create a new one
+ var challenge = TransactionRequestChallenge(id = java.util.UUID.randomUUID().toString, allowed_attempts = 3, challenge_type = TransactionRequests.CHALLENGE_SANDBOX_TAN)
+ saveTransactionRequestChallenge(result.get.id, challenge)
+ result = Full(result.get.copy(challenge = challenge))
+ }
+
+ result
+ }
+
+
+ def createTransactionRequestv200(initiator : User, fromAccount : BankAccount, toAccount: BankAccount, transactionRequestType: TransactionRequestType, body: TransactionRequestBody) : Box[TransactionRequest] = {
+ //set initial status
+ //for sandbox / testing: depending on amount, we ask for challenge or not
+ val status =
+ if (transactionRequestType.value == TransactionRequests.CHALLENGE_SANDBOX_TAN && BigDecimal(body.value.amount) < 1000) {
+ TransactionRequests.STATUS_COMPLETED
+ } else {
+ TransactionRequests.STATUS_INITIATED
+ }
+
+
+ // Always create a new Transaction Request
+ var result = for {
+ 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 <- 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.
+ isPositiveAmtToSend <- booleanToBox(rawAmt > BigDecimal("0"), s"Can't send a payment with a value of 0 or less. (${rawAmt})")
+
+
+ // For now, arbitary charge value to demonstrate PSD2 charge transparency principle. Eventually this would come from Transaction Type? 10 decimal places of scaling so can add small percentage per transaction.
+ chargeValue <- tryo {(BigDecimal(body.value.amount) * 0.0001).setScale(10, BigDecimal.RoundingMode.HALF_UP).toDouble} ?~! s"could not create charge for ${body.value.amount}"
+ charge = TransactionRequestCharge("Total charges for completed transaction", AmountOfMoney(body.value.currency, chargeValue.toString()))
+
+
+
+ transactionRequest <- createTransactionRequestImpl(TransactionRequestId(java.util.UUID.randomUUID().toString), transactionRequestType, fromAccount, toAccount, body, status, charge)
+ } yield transactionRequest
+
+ //make sure we get something back
+ result = Full(result.openOrThrowException("Exception: Couldn't create transactionRequest"))
+
+ // If no challenge necessary, create Transaction immediately and put in data store and object to return
+ if (status == TransactionRequests.STATUS_COMPLETED) {
+ val createdTransactionId = Connector.connector.vend.makePaymentv200(initiator, BankAccountUID(fromAccount.bankId, fromAccount.accountId),
+ BankAccountUID(toAccount.bankId, toAccount.accountId), BigDecimal(body.value.amount), body.description)
+
+ //set challenge to null
+ result = Full(result.get.copy(challenge = null))
+
+ //save transaction_id if we have one
+ createdTransactionId match {
+ case Full(ti) => {
+ if (! createdTransactionId.isEmpty) {
+ saveTransactionRequestTransaction(result.get.id, ti)
+ result = Full(result.get.copy(transaction_ids = ti.value))
+ }
+ }
+ case _ => None
+ }
+ } else {
+ //if challenge necessary, create a new one
+ var challenge = TransactionRequestChallenge(id = java.util.UUID.randomUUID().toString, allowed_attempts = 3, challenge_type = TransactionRequests.CHALLENGE_SANDBOX_TAN)
+ saveTransactionRequestChallenge(result.get.id, challenge)
+ result = Full(result.get.copy(challenge = challenge))
+ }
+
+ result
+ }
+
+ def createTransactionRequestv210(initiator : User, fromAccount : BankAccount, toAccount: Box[BankAccount], transactionRequestType: TransactionRequestType, details: TransactionRequestDetails, detailsPlain: String) : Box[TransactionRequest210] = {
+ //set initial status
+ //for sandbox / testing: depending on amount, we ask for challenge or not
+ val status =
+ if (transactionRequestType.value == TransactionRequests.CHALLENGE_SANDBOX_TAN && BigDecimal(details.value.amount) < 1000) {
+ TransactionRequests.STATUS_COMPLETED
+ } else {
+ TransactionRequests.STATUS_INITIATED
+ }
+
+
+ // Always create a new Transaction Request
+ var result = for {
+ 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)
+
+ rawAmt <- tryo { BigDecimal(details.value.amount) } ?~! s"amount ${details.value.amount} not convertible to number"
+ // isValidTransactionRequestType is checked at API layer. Maybe here too.
+ isPositiveAmtToSend <- booleanToBox(rawAmt > BigDecimal("0"), s"Can't send a payment with a value of 0 or less. (${rawAmt})")
+
+
+ // For now, arbitary charge value to demonstrate PSD2 charge transparency principle. Eventually this would come from Transaction Type? 10 decimal places of scaling so can add small percentage per transaction.
+ chargeValue <- tryo {(BigDecimal(details.value.amount) * 0.0001).setScale(10, BigDecimal.RoundingMode.HALF_UP).toDouble} ?~! s"could not create charge for ${details.value.amount}"
+ charge = TransactionRequestCharge("Total charges for completed transaction", AmountOfMoney(details.value.currency, chargeValue.toString()))
+
+
+
+ transactionRequest <- createTransactionRequestImpl210(TransactionRequestId(java.util.UUID.randomUUID().toString), transactionRequestType, fromAccount, detailsPlain, status, charge)
+ } yield transactionRequest
+
+ //make sure we get something back
+ result = Full(result.openOrThrowException("Exception: Couldn't create transactionRequest"))
+
+ // If no challenge necessary, create Transaction immediately and put in data store and object to return
+ if (status == TransactionRequests.STATUS_COMPLETED) {
+ val createdTransactionId = transactionRequestType.value match {
+ case "SANDBOX_TAN" => Connector.connector.vend.makePaymentv200(initiator, BankAccountUID(fromAccount.bankId, fromAccount.accountId),
+ BankAccountUID(toAccount.get.bankId, toAccount.get.accountId), BigDecimal(details.value.amount), details.asInstanceOf[TransactionRequestDetailsSandBoxTan].description)
+ case "SEPA" => Empty
+ }
+
+ //set challenge to null
+ result = Full(result.get.copy(challenge = null))
+
+ //save transaction_id if we have one
+ createdTransactionId match {
+ case Full(ti) => {
+ if (! createdTransactionId.isEmpty) {
+ saveTransactionRequestTransaction(result.get.id, ti)
+ result = Full(result.get.copy(transaction_ids = ti.value))
+ }
+ }
+ case _ => None
+ }
+ } else {
+ //if challenge necessary, create a new one
+ var challenge = TransactionRequestChallenge(id = java.util.UUID.randomUUID().toString, allowed_attempts = 3, challenge_type = TransactionRequests.CHALLENGE_SANDBOX_TAN)
+ saveTransactionRequestChallenge(result.get.id, challenge)
+ result = Full(result.get.copy(challenge = challenge))
+ }
+
+ result
+ }
+
+
+
+
+
+ //place holder for various connector methods that overwrite methods like these, does the actual data access
+ protected def createTransactionRequestImpl(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ fromAccount : BankAccount, counterparty : BankAccount, body: TransactionRequestBody,
+ status: String, charge: TransactionRequestCharge) : Box[TransactionRequest]
+
+ protected def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ fromAccount : BankAccount, details: String,
+ status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210]
+
+
+ def saveTransactionRequestTransaction(transactionRequestId: TransactionRequestId, transactionId: TransactionId) = {
+ //put connector agnostic logic here if necessary
+ saveTransactionRequestTransactionImpl(transactionRequestId, transactionId)
+ }
+
+ protected def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId): Box[Boolean]
+
+ def saveTransactionRequestChallenge(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge) = {
+ //put connector agnostic logic here if necessary
+ saveTransactionRequestChallengeImpl(transactionRequestId, challenge)
+ }
+
+ protected def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge): Box[Boolean]
+
+ protected def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String): Box[Boolean]
+
+ def getTransactionRequests(initiator : User, fromAccount : BankAccount) : Box[List[TransactionRequest]] = {
+ val transactionRequests =
+ for {
+ fromAccount <- 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")
+ transactionRequests <- getTransactionRequestsImpl(fromAccount)
+ } yield transactionRequests
+
+ //make sure we return null if no challenge was saved (instead of empty fields)
+ if (!transactionRequests.isEmpty) {
+ Full(
+ transactionRequests.get.map(tr => if (tr.challenge.id == "") {
+ tr.copy(challenge = null)
+ } else {
+ tr
+ })
+ )
+ } else {
+ transactionRequests
+ }
+ }
+
+ def getTransactionRequests210(initiator : User, fromAccount : BankAccount) : Box[List[TransactionRequest210]] = {
+ val transactionRequests =
+ for {
+ fromAccount <- 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")
+ transactionRequests <- getTransactionRequestsImpl210(fromAccount)
+ } yield transactionRequests
+
+ //make sure we return null if no challenge was saved (instead of empty fields)
+ if (!transactionRequests.isEmpty) {
+ Full(
+ transactionRequests.get.map(tr => if (tr.challenge.id == "") {
+ tr.copy(challenge = null)
+ } else {
+ tr
+ })
+ )
+ } else {
+ transactionRequests
+ }
+ }
+
+ protected def getTransactionRequestsImpl(fromAccount : BankAccount) : Box[List[TransactionRequest]]
+
+ protected def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]]
+
+ protected def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest]
+
+ def getTransactionRequestTypes(initiator : User, fromAccount : BankAccount) : Box[List[TransactionRequestType]] = {
+ for {
+ fromAccount <- 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")
+ transactionRequestTypes <- getTransactionRequestTypesImpl(fromAccount)
+ } yield transactionRequestTypes
+ }
+
+ protected def getTransactionRequestTypesImpl(fromAccount : BankAccount) : Box[List[TransactionRequestType]]
+
+
+ def answerTransactionRequestChallenge(transReqId: TransactionRequestId, answer: String) : Box[Boolean] = {
+ val tr = getTransactionRequestImpl(transReqId) ?~ "Transaction Request not found"
+
+ tr match {
+ case Full(tr: TransactionRequest) =>
+ if (tr.challenge.allowed_attempts > 0) {
+ if (tr.challenge.challenge_type == TransactionRequests.CHALLENGE_SANDBOX_TAN) {
+ //check if answer supplied is correct (i.e. for now, TAN -> some number and not empty)
+ for {
+ nonEmpty <- booleanToBox(answer.nonEmpty) ?~ "Need a non-empty answer"
+ answerToNumber <- tryo(BigInt(answer)) ?~! "Need a numeric TAN"
+ positive <- booleanToBox(answerToNumber > 0) ?~ "Need a positive TAN"
+ } yield true
+
+ //TODO: decrease allowed attempts value
+ }
+ //else if (tr.challenge.challenge_type == ...) {}
+ else {
+ Failure("unknown challenge type")
+ }
+ } else {
+ Failure("Sorry, you've used up your allowed attempts.")
+ }
+ case Failure(f, Empty, Empty) => Failure(f)
+ case _ => Failure("Error getting Transaction Request")
+ }
+ }
+
+ def createTransactionAfterChallenge(initiator: User, transReqId: TransactionRequestId) : Box[TransactionRequest] = {
+ for {
+ tr <- getTransactionRequestImpl(transReqId) ?~ "Transaction Request not found"
+ transId <- makePayment(initiator, BankAccountUID(BankId(tr.from.bank_id), AccountId(tr.from.account_id)),
+ BankAccountUID (BankId(tr.body.to.bank_id), AccountId(tr.body.to.account_id)), BigDecimal (tr.body.value.amount), tr.body.description) ?~ "Couldn't create Transaction"
+ didSaveTransId <- saveTransactionRequestTransaction(transReqId, transId)
+ didSaveStatus <- saveTransactionRequestStatusImpl(transReqId, TransactionRequests.STATUS_COMPLETED)
+ //get transaction request again now with updated values
+ tr <- getTransactionRequestImpl(transReqId)
+ } yield {
+ tr
+ }
+ }
+
+ def createTransactionAfterChallengev200(initiator: User, transReqId: TransactionRequestId) : Box[TransactionRequest] = {
+ for {
+ tr <- getTransactionRequestImpl(transReqId) ?~ "Transaction Request not found"
+ transId <- makePaymentv200(initiator, BankAccountUID(BankId(tr.from.bank_id), AccountId(tr.from.account_id)),
+ BankAccountUID (BankId(tr.body.to.bank_id), AccountId(tr.body.to.account_id)), BigDecimal (tr.body.value.amount), tr.body.description) ?~ "Couldn't create Transaction"
+ didSaveTransId <- saveTransactionRequestTransaction(transReqId, transId)
+ didSaveStatus <- saveTransactionRequestStatusImpl(transReqId, TransactionRequests.STATUS_COMPLETED)
+ //get transaction request again now with updated values
+ tr <- getTransactionRequestImpl(transReqId)
+ } yield {
+ tr
+ }
+ }
+ /*
+ non-standard calls --do not make sense in the regular context but are used for e.g. tests
+ */
+
+ //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,
+ accountType: String, accountLabel:String, currency: String, accountHolderName : String) : (Bank, BankAccount)
+
+ //generates an unused account number and then creates the sandbox account using that number
+ def createSandboxBankAccount(bankId : BankId, accountId : AccountId, accountType: String, accountLabel: String, currency : String, initialBalance : BigDecimal, accountHolderName : String) : Box[BankAccount] = {
+ val uniqueAccountNumber = {
+ def exists(number : String) = Connector.connector.vend.accountExists(bankId, number)
+
+ def appendUntilOkay(number : String) : String = {
+ val newNumber = number + Random.nextInt(10)
+ if(!exists(newNumber)) newNumber
+ else appendUntilOkay(newNumber)
+ }
+
+ //generates a random 8 digit account number
+ val firstTry = (Random.nextDouble() * 10E8).toInt.toString
+ appendUntilOkay(firstTry)
+ }
+
+ createSandboxBankAccount(
+ bankId,
+ accountId,
+ uniqueAccountNumber,
+ accountType,
+ accountLabel,
+ currency,
+ initialBalance,
+ accountHolderName
+ )
+
+ }
+
+ //creates a bank account for an existing bank, with the appropriate values set. Can fail if the bank doesn't exist
+ def createSandboxBankAccount(bankId : BankId, accountId : AccountId, accountNumber: String,
+ accountType: String, accountLabel: String, currency : String,
+ initialBalance : BigDecimal, accountHolderName : String) : Box[BankAccount]
+
+ //sets a user as an account owner/holder
+ def setAccountHolder(bankAccountUID: BankAccountUID, user : User) : Unit
+
+ //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 and associated transactions
+ def removeAccount(bankId: BankId, accountId: AccountId) : Boolean
+
+ //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
+ def createImportedTransaction(transaction: ImporterTransaction) : Box[Transaction]
+ def updateAccountBalance(bankId : BankId, accountId : AccountId, newBalance : BigDecimal) : Boolean
+ def setBankAccountLastUpdated(bankNationalIdentifier: String, accountNumber : String, updateDate: Date) : Boolean
+
+ def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean
+
+ }
\ No newline at end of file
diff --git a/src/main/scala/code/bankconnectors/KafkaHelper.scala b/src/main/scala/code/bankconnectors/KafkaHelper.scala
new file mode 100644
index 000000000..fe2964612
--- /dev/null
+++ b/src/main/scala/code/bankconnectors/KafkaHelper.scala
@@ -0,0 +1,174 @@
+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 kafka.consumer.{Consumer, _}
+import kafka.message._
+import kafka.producer.{KeyedMessage, Producer, ProducerConfig}
+import kafka.utils.Json
+import net.liftweb.common.Loggable
+import net.liftweb.json
+import net.liftweb.json._
+import net.liftweb.util.Props
+
+
+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"),
+ val delay: Long = 0) extends Loggable {
+
+ val zkProps = new Properties()
+ zkProps.put("log4j.logger.org.apache.zookeeper", "ERROR")
+ org.apache.log4j.PropertyConfigurator.configure(zkProps)
+
+ 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("auto.commit.enable", "true")
+ props.put("zookeeper.sync.time.ms", "2000")
+ 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")
+ val config = new ConsumerConfig(props)
+ config
+ }
+
+ 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
+ val consumerMap = consumer.createMessageStreams(Map(topic -> 1))
+
+ 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()
+ // skip null entries
+ if (mIt != null && mIt.key != null && mIt.message != null) {
+ val msg = new String(mIt.message(), "UTF8")
+ val key = new String(mIt.key(), "UTF8")
+ // check if the id matches
+ if (key == reqId) {
+ // Parse JSON message
+ val j = json.parse(msg)
+ // disconnect from Kafka
+ consumer.shutdown()
+ // return as JSON
+ return j
+ }
+ } else {
+ logger.warn("KafkaConsumer: Got null value/key from kafka. Might be south-side connector issue.")
+ }
+ }
+ return json.parse("""{"error":"KafkaConsumer could not fetch response"}""") //TODO: replace with standard message
+ }
+ catch {
+ case e:kafka.consumer.ConsumerTimeoutException =>
+ logger.error("KafkaConsumer: timeout")
+ return json.parse("""{"error":"KafkaConsumer timeout"}""") //TODO: replace with standard message
+ }
+ }
+ // disconnect from kafka
+ consumer.shutdown()
+ logger.info("KafkaProducer: shutdown")
+ return json.parse("""{"info":"KafkaConsumer shutdown"}""") //TODO: replace with standard message
+ }
+}
+
+
+class KafkaProducer(
+ topic: String = Props.get("kafka.request_topic").openOrThrowException("no kafka.request_topic set"),
+ brokerList: String = Props.get("kafka.host")openOr("localhost:9092"),
+ clientId: String = UUID.randomUUID().toString,
+ synchronously: Boolean = true,
+ compress: Boolean = true,
+ batchSize: Integer = 200,
+ messageSendMaxRetries: Integer = 3,
+ requestRequiredAcks: Integer = -1
+ ) extends Loggable {
+
+
+ // 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)
+
+ // 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)
+ }
+ }
+
+ implicit val formats = DefaultFormats
+
+ def send(key: String, request: String, arguments: Map[String, String], partition: String = null): Boolean = {
+ // create message using request and arguments strings
+ val reqCommand = Map(request -> arguments)
+ val message = Json.encode(reqCommand)
+ // 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]): Boolean = {
+ try {
+ // actually send the message to kafka
+ producer.send(kafkaMesssage(key, message, partition))
+ } catch {
+ case e: kafka.common.FailedToSendMessageException =>
+ logger.error("KafkaProducer: Failed to send message")
+ return false
+ case e: Throwable =>
+ logger.error("KafkaProducer: Unknown error while trying to send message")
+ e.printStackTrace()
+ return false
+ }
+ true
+ }
+
+}
diff --git a/src/main/scala/code/bankconnectors/KafkaLibMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaLibMappedConnector.scala
new file mode 100644
index 000000000..935ff2a52
--- /dev/null
+++ b/src/main/scala/code/bankconnectors/KafkaLibMappedConnector.scala
@@ -0,0 +1,1049 @@
+//package code.bankconnectors
+//
+//import java.text.SimpleDateFormat
+//import java.util.{Optional, Date, Locale, UUID}
+//
+//import code.api.util.ErrorMessages
+//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._
+//import code.model.dataAccess._
+//import code.sandbox.{CreateViewImpls, Saveable}
+//import code.transaction.MappedTransaction
+//import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest}
+//import code.transactionrequests.TransactionRequests._
+//import code.util.{Helper, TTLCache}
+//import code.views.Views
+//import com.tesobe.obp.kafka.SimpleNorth
+//import com.tesobe.obp.transport.Transport
+//import com.tesobe.obp.transport.Transport.Factory
+//import net.liftweb.common._
+//import net.liftweb.json
+//import net.liftweb.mapper._
+//import net.liftweb.util.Helpers._
+//import net.liftweb.util.Props
+//import net.liftweb.json._
+//
+//import scala.collection.JavaConversions._
+//
+//object KafkaLibMappedConnector extends Connector with CreateViewImpls with Loggable {
+// type JBank = com.tesobe.obp.transport.Bank
+// type JTransaction = com.tesobe.obp.transport.Transaction
+// type JAccount = com.tesobe.obp.transport.Account
+// type OutboundContext = com.tesobe.obp.transport.OutboundContext
+//
+// val version = getVersion()
+//
+// val factory : Factory = Transport.factory(version, Transport.Encoding.json).get
+// //todo get topic names from the props
+// val north: SimpleNorth = new SimpleNorth("Request", "Response") // Kafka
+// val connector : com.tesobe.obp.transport.Connector = factory.connector(north)
+//
+// north.receive() // start Kafka
+//
+// var producer = new KafkaProducer()
+// var consumer = new KafkaConsumer()
+// type AccountType = KafkaBankAccount
+//
+// // 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)
+// val cachedBanks = TTLCache[List[KafkaInboundBank]](cacheTTL)
+// val cachedAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL)
+// val cachedPublicAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL)
+// val cachedUserAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL)
+//
+// implicit val formats = net.liftweb.json.DefaultFormats
+//
+// def getVersion(): Transport.Version = {
+// val versionProp = Props.get("connector").openOrThrowException("no connector set")
+// val version = versionProp.split("_").last
+//
+// version.toLowerCase match {
+// case "v0" => Transport.Version.v0
+// case _ => Transport.Version.v0
+// }
+// }
+//
+// //gets user handled by this connector
+// def getUser( username: String, password: String ): Box[KafkaInboundUser] = {
+// val user = connector.getUser(username, new OutboundContext(null, null, null)).user
+//
+// if (user.isPresent) {
+// val u = user.get
+// Full(new KafkaInboundUser(u.email(), u.password(), u.email()))
+// } else {
+// Empty
+// }
+// }
+//
+// //gets banks handled by this connector
+// override def getBanks: List[Bank] = {
+// val banks = connector.getBanks(new OutboundContext(null, null, null)).banks
+//
+// //Loop through list of responses and create entry for each
+// val res = {
+// for (r <- banks) yield {
+// KafkaBank(KafkaInboundBank(r.id, r.shortName, r.fullName, r.logo, r.url))
+// }
+// }
+// // Return list of results
+//
+// logger.debug(s"Kafka getBanks says res is $res")
+// res.toList
+// }
+//
+// // Gets bank identified by bankId
+// override def getBank(id: BankId): Box[Bank] = {
+// val bank = connector.getBank(id.value, new OutboundContext(null, null, null)).bank
+//
+// if(bank.isPresent) {
+// val b = bank.get
+// Full(KafkaBank(KafkaInboundBank(b.id, b.shortName, b.fullName, b.logo, b.url)))
+// } else {
+// Empty
+// }
+// }
+//
+// // Gets transaction identified by bankid, accountid and transactionId
+// def getTransaction(bankId: BankId, accountID: AccountId, transactionId: TransactionId): Box[Transaction] = {
+// val transaction = connector.getTransaction(bankId.value, accountID.value, transactionId.value, new OutboundContext(null, null, null)).transaction
+//
+// if(transaction.isPresent) {
+// val t = transaction.get
+// val kafkaInboundTransaction = new KafkaInboundTransaction(t.id(),
+// new KafkaInboundAccountId(t.account_number(), t.bank()),
+// Some(new KafkaInboundTransactionCounterparty(Some(t.name()), Some(t.account_number()))),
+// new KafkaInboundTransactionDetails(t.`type`(), t.description(), t.posted().toString, t.completed().toString, t.new_balance(), t.value())
+// )
+//
+// if (transactionId.value == t.id()){
+// createNewTransaction(kafkaInboundTransaction)
+// } else {
+// Failure(ErrorMessages.InvalidGetTransactionConnectorResponse, Empty, Empty)
+// }
+// } else {
+// Empty
+// }
+// }
+//
+// // Gets transaction identified by bankid and accountid
+// override def getTransactions(bankId: BankId, accountID: AccountId, queryParams: OBPQueryParam*): Box[List[Transaction]] = {
+// val transactions = connector.getTransactions(bankId.value, accountID.value, new OutboundContext(null, null, null)).transactions
+//
+// // Check does the response data match the requested data
+// val isCorrect = transactions.forall(x => x.account_number() == accountID.value && x.bank() == bankId.value)
+// if (!isCorrect) throw new Exception(ErrorMessages.InvalidGetTransactionsConnectorResponse)
+// // Populate fields and generate result
+// val res = for {
+// t <- transactions
+// kafkaInboundTransaction = new KafkaInboundTransaction(t.id(),
+// new KafkaInboundAccountId(t.account_number(), t.bank()),
+// Some(new KafkaInboundTransactionCounterparty(Some(t.name()), Some(t.account_number()))),
+// new KafkaInboundTransactionDetails(t.`type`(), t.description(), t.posted().toString, t.completed().toString, t.new_balance(), t.value())
+// )
+// transaction <- createNewTransaction(kafkaInboundTransaction)
+// } yield {
+// transaction
+// }
+// Full(res.toList)
+// }
+//
+// // Gets account identified by bankid and accountid
+// override def getBankAccount(bankId: BankId, accountID: AccountId): Box[KafkaBankAccount] = {
+// val account = connector.getAccount(bankId.value, accountID.value, new OutboundContext(null, null, null)).account
+//
+// if (account.isPresent) {
+// val a = account.get
+// // Check does the response data match the requested data
+// val accResp = List((BankId(a.bank()), AccountId(a.id()))).toSet
+// val acc = List((bankId, accountID)).toSet
+// if ((accResp diff acc).size > 0) throw new Exception(ErrorMessages.InvalidGetBankAccountConnectorResponse)
+//
+// createMappedAccountDataIfNotExisting(a.bank(), a.id(), a.label())
+//
+// Full(new KafkaBankAccount(new KafkaInboundAccount(a.id(), a.bank(), a.label(), a.number(), a.`type`(),
+// new KafkaInboundBalance(a.currency(), a.amount()), a.iban(), Nil, false, false, false))) //TODO figure out the generate parameters
+// } else {
+// Empty
+// }
+// }
+//
+// // Gets accounts identified by bankid and accountid
+// override def getBankAccounts(accts: List[(BankId, AccountId)]): List[KafkaBankAccount] = {
+// var accounts: List[JAccount] = Nil
+//
+// for (ba <- accts){
+// val account = connector.getAccount(ba._1.value, ba._2.value, new OutboundContext(null, null, null)).account
+// if (account.isPresent){
+// accounts = accounts ::: List[JAccount](account.get)
+// }
+// }
+//
+// // Check does the response data match the requested data
+// val accRes = for(row <- accounts) yield {
+// (BankId(row.bank), AccountId(row.id))
+// }
+// if ((accRes.toSet diff accts.toSet).size > 0) throw new Exception(ErrorMessages.InvalidGetBankAccountsConnectorResponse)
+//
+// accounts.map { a =>
+// createMappedAccountDataIfNotExisting(a.bank, a.id, a.label)
+// new KafkaBankAccount(new KafkaInboundAccount(a.id(), a.bank(), a.label(), a.number(), a.`type`(),
+// new KafkaInboundBalance(a.currency(), a.amount()), a.iban(), Nil, false, false, false)) }
+// }
+//
+// private def getAccountByNumber(bankId : BankId, number : String) : Box[AccountType] = {
+// // Generate random uuid to be used as request-respose match id
+// val reqId: String = UUID.randomUUID().toString
+// // Create argument list with reqId
+// // in order to fetch corresponding response
+// val argList = Map("bankId" -> bankId.toString,
+// "username" -> OBPUser.getCurrentUserUsername,
+// "number" -> number)
+// // 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[KafkaInboundAccount])
+// }
+// createMappedAccountDataIfNotExisting(r.bank, r.id, r.label)
+// Full(new KafkaBankAccount(r))
+// }
+//
+// 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)
+// }
+//
+// /**
+// *
+// * 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 <- getBank(bankId)
+// account <- getBankAccountType(bankId, accountID)
+// } {
+// spawn{
+// val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false)
+// val outDatedTransactions = Box!!account.lastUpdate match {
+// case Full(l) => now after time(l.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1)))
+// case _ => true
+// }
+// //if(outDatedTransactions && useMessageQueue) {
+// // UpdatesRequestSender.sendMsg(UpdateBankAccount(account.number, bank.national_identifier.get))
+// //}
+// }
+// }
+// }
+// */
+//
+// //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
+//
+//
+// // 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: AccountType, toAccount: AccountType, 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 : 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 reqId: String = UUID.randomUUID().toString
+// // Create argument list with reqId
+// // in order to fetch corresponding response
+// val argObj = KafkaOutboundTransaction(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 argMap = Extraction.decompose(argObj).values
+// val r = process(reqId, "saveTransaction", argMap.asInstanceOf[Map[String,String]]) //.extract[KafkaInboundTransactionId]
+//
+// r.extract[KafkaInboundTransactionId] match {
+// case r: KafkaInboundTransactionId => Full(TransactionId(r.transactionId))
+// case _ => Full(TransactionId("0"))
+// }
+//
+// }
+//
+// /*
+// Transaction Requests
+// */
+//
+// override def createTransactionRequestImpl(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+// account : BankAccount, counterparty : BankAccount, body: TransactionRequestBody,
+// status: String, charge: TransactionRequestCharge) : 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 createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+// account : BankAccount, details: String,
+// status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = {
+// val mappedTransactionRequest = MappedTransactionRequest210.create
+// .mTransactionRequestId(transactionRequestId.value)
+// .mType(transactionRequestType.value)
+// .mFrom_BankId(account.bankId.value)
+// .mFrom_AccountId(account.accountId.value)
+// .mDetails(details)
+// .mStatus(status)
+// .mStartDate(now)
+// .mEndDate(now).saveMe
+// Full(mappedTransactionRequest).flatMap(_.toTransactionRequest210)
+// }
+//
+// 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 getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = {
+// val transactionRequests = MappedTransactionRequest210.findAll(By(MappedTransactionRequest210.mFrom_AccountId, fromAccount.accountId.value),
+// By(MappedTransactionRequest210.mFrom_BankId, fromAccount.bankId.value))
+//
+// Full(transactionRequests.flatMap(_.toTransactionRequest210))
+// }
+//
+// 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,
+// accountType: String, accountLabel: String, currency: String, accountHolderName: String): (Bank, BankAccount) = {
+// //don't require and exact match on the name, just the identifier
+// val bank: 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,
+// accountType,
+// accountLabel,
+// currency,
+// 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 = {
+// getAccountByNumber(bankId, accountNumber) != null
+// }
+//
+// //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 = getBankAccount(bankId, accountID)
+//
+// val accountDeleted = account match {
+// case acc => true //acc.delete_! //TODO
+// 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,
+// accountType: String, accountLabel: 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, accountType, accountLabel, currency, balanceInSmallestCurrencyUnits, accountHolderName)
+// }
+//
+// }
+//
+// //sets a user as an account owner/holder
+// override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = {
+// MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankAccountUID.accountId.value, bankAccountUID.bankId.value)
+// }
+//
+// private def createAccountIfNotExisting(bankId: BankId, accountID: AccountId, accountNumber: String,
+// accountType: String, accountLabel: String, currency: String,
+// balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = {
+// 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
+// case _ => null //TODO
+// /*
+// new KafkaBankAccount
+// .bank(bankId.value)
+// .theAccountId(accountID.value)
+// .accountNumber(accountNumber)
+// .accountType(accountType)
+// .accountLabel(accountLabel)
+// .accountCurrency(currency)
+// .accountBalance(balanceInSmallestCurrencyUnits)
+// .holder(accountHolderName)
+// .saveMe()
+// */
+// }
+// }
+//
+// private def createMappedAccountDataIfNotExisting(bankId: String, accountId: String, label: String) : Boolean = {
+// MappedKafkaBankAccountData.find(By(MappedKafkaBankAccountData.accountId, accountId),
+// By(MappedKafkaBankAccountData.bankId, bankId)) match {
+// case Empty =>
+// val data = new MappedKafkaBankAccountData
+// data.setAccountId(accountId)
+// data.setBankId(bankId)
+// data.setLabel(label)
+// data.save()
+// true
+// case _ =>
+// logger.info(s"account data with id $accountId at bank with id $bankId already exists. No need to create a new one.")
+// false
+// }
+// }
+//
+// /*
+// End of bank account creation
+// */
+//
+//
+// /*
+// 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 <- getBankAccount(bankId, accountID)
+// bank <- getBank(bankId)
+// } yield {
+// //acc.balance = newBalance
+// 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 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 = 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
+// }
+// }
+// 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 <- getBankAccount(bankId, accountID)
+// bank <- getBank(bankId)
+// d <- MappedKafkaBankAccountData.find(By(MappedKafkaBankAccountData.accountId, accountID.value), By(MappedKafkaBankAccountData.bankId, bank.bankId.value))
+// } yield {
+// d.setLabel(label)
+// d.save()
+// }
+// result.getOrElse(false)
+// }
+//
+//
+//
+//
+// /////////////////////////////////////////////////////////////////////////////
+//
+//
+//
+// 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
+// consumer = new KafkaConsumer()
+// }
+// retries = 3
+// while (producer == null && retries > 0) {
+// retries -= 1
+// producer = new KafkaProducer()
+// }
+// if (producer == null || consumer == null)
+// return json.parse("""{"error":"connection failed. try again later."}""")
+// // Send request to Kafka
+// producer.send(reqId, command, argList, "1")
+// // Request sent, now we wait for response with the same reqId
+// val res = consumer.getResponse(reqId)
+// res
+// }
+//
+//
+// // Helper for creating a transaction
+// 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)
+//
+// var dateCompleted: Date = null
+// 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)
+//
+// for {
+// 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.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.get, 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)
+// )
+// }
+// }
+//
+//
+// case class KafkaBank(r: KafkaInboundBank) extends Bank {
+// def fullName = r.full_name
+// def shortName = r.short_name
+// def logoUrl = r.logo
+// def bankId = BankId(r.id)
+// def nationalIdentifier = "None" //TODO
+// def swiftBic = "None" //TODO
+// def websiteUrl = r.website
+// }
+//
+// // Helper for creating other bank account
+// def createOtherBankAccount(c: KafkaInboundTransactionCounterparty, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = {
+// new OtherBankAccount(
+// id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""),
+// label = c.account_number.getOrElse(c.name.getOrElse("")),
+// nationalIdentifier = "",
+// swift_bic = None,
+// iban = None,
+// number = c.account_number.getOrElse(""),
+// bankName = "",
+// kind = "",
+// originalPartyBankId = BankId(o.bankId.value),
+// originalPartyAccountId = AccountId(o.accountId.value),
+// alreadyFoundMetadata = alreadyFoundMetadata
+// )
+// }
+//
+// case class KafkaBankAccount(r: KafkaInboundAccount) extends BankAccount {
+// def accountId : AccountId = AccountId(r.id)
+// def accountType : String = r.`type`
+// def balance : BigDecimal = BigDecimal(r.balance.amount)
+// def currency : String = r.balance.currency
+// def name : String = r.owners.head
+// def swift_bic : Option[String] = Some("swift_bic") //TODO
+// def iban : Option[String] = Some(r.IBAN)
+// def number : String = r.number
+// def bankId : BankId = BankId(r.bank)
+// def lastUpdate : Date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(today.getTime.toString)
+// def accountHolder : String = r.owners.head
+//
+// // Fields modifiable from OBP are stored in mapper
+// def label : String = (for {
+// d <- MappedKafkaBankAccountData.find(By(MappedKafkaBankAccountData.accountId, r.id))
+// } yield {
+// d.getLabel
+// }).getOrElse(r.number)
+//
+// }
+//
+//
+// case class KafkaInboundBank(
+// id : String,
+// short_name : String,
+// full_name : String,
+// logo : String,
+// website : String)
+//
+//
+// /** 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 KafkaInboundLicense(
+// id : String,
+// name : String)
+//
+// case class KafkaInboundMeta(
+// license : KafkaInboundLicense)
+//
+// case class KafkaInboundLobby(
+// hours : String)
+//
+// case class KafkaInboundDriveUp(
+// hours : String)
+//
+// /**
+// *
+// * @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,
+// city : String,
+// county : String, // Division of State
+// state : String, // Division of Country
+// post_code : String,
+// country_code: String)
+//
+// case class KafkaInboundLocation(
+// latitude : Double,
+// longitude : Double)
+//
+// case class KafkaInboundUser(
+// email : String,
+// password : String,
+// display_name : String)
+//
+// case class KafkaInboundValidatedUser(
+// email : String,
+// display_name : String)
+//
+// 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 KafkaInboundBalance(
+// currency : String,
+// amount : String)
+//
+// case class KafkaInboundTransaction(
+// id : String,
+// this_account : KafkaInboundAccountId,
+// counterparty : Option[KafkaInboundTransactionCounterparty],
+// details : KafkaInboundTransactionDetails)
+//
+// case class KafkaInboundTransactionCounterparty(
+// name : Option[String], // Also known as Label
+// account_number : Option[String])
+//
+// case class KafkaInboundAccountId(
+// id : String,
+// bank : String)
+//
+// case class KafkaInboundTransactionDetails(
+// `type` : String,
+// description : String,
+// posted : String,
+// completed : String,
+// new_balance : String,
+// value : String)
+//
+//
+// case class KafkaInboundAtm(
+// id : String,
+// bank_id: String,
+// name : String,
+// address : KafkaInboundAddress,
+// location : KafkaInboundLocation,
+// meta : KafkaInboundMeta
+// )
+//
+//
+// case class KafkaInboundProduct(
+// bank_id : String,
+// code: String,
+// name : String,
+// category : String,
+// family : String,
+// super_family : String,
+// more_info_url : String,
+// meta : KafkaInboundMeta
+// )
+//
+//
+// case class KafkaInboundAccountData(
+// banks : List[KafkaInboundBank],
+// users : List[KafkaInboundUser],
+// accounts : List[KafkaInboundAccount]
+// )
+//
+// // We won't need this. TODO clean up.
+// 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 KafkaInboundCrmEvent(
+// id : String, // crmEventId
+// bank_id : String,
+// customer: KafkaInboundCustomer,
+// category : String,
+// detail : String,
+// channel : String,
+// actual_date: String
+// )
+//
+// case class KafkaInboundCustomer(
+// name: String,
+// number : String // customer number, also known as ownerId (owner of accounts) aka API User?
+// )
+//
+//
+// case class KafkaInboundTransactionId(
+// transactionId : String
+// )
+// case class KafkaOutboundTransaction(username: String,
+// accountId: String,
+// currency: String,
+// amount: String,
+// otherAccountId: String,
+// otherAccountCurrency: String,
+// transactionType: String)
+//}
+//
diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala
new file mode 100644
index 000000000..2821861af
--- /dev/null
+++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala
@@ -0,0 +1,1155 @@
+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
+import code.metadata.narrative.MappedNarrative
+import code.metadata.tags.MappedTag
+import code.metadata.transactionimages.MappedTransactionImage
+import code.metadata.wheretags.MappedWhereTag
+import code.model._
+import code.model.dataAccess._
+import code.sandbox.{CreateViewImpls, Saveable}
+import code.transaction.MappedTransaction
+import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest}
+import code.transactionrequests.TransactionRequests._
+import code.util.{Helper, TTLCache}
+import code.views.Views
+import net.liftweb.common._
+import net.liftweb.json
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers._
+import net.liftweb.util.Props
+import net.liftweb.json._
+
+object KafkaMappedConnector extends Connector with CreateViewImpls with Loggable {
+
+ var producer = new KafkaProducer()
+ var consumer = new KafkaConsumer()
+ type AccountType = KafkaBankAccount
+
+ // 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)
+ val cachedBanks = TTLCache[List[KafkaInboundBank]](cacheTTL)
+ val cachedAccounts = TTLCache[List[KafkaInboundAccount]](cacheTTL)
+ 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 )}
+ // 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}
+ } yield {
+ if (username == u.email) new KafkaInboundUser( recEmail, password, recEmail)
+ else null
+ }
+ }
+
+ 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 = {
+ if (account.owners.contains(owner)) {
+ val apiUserOwner = APIUser.findAll.find(user => owner == user.emailAddress)
+ apiUserOwner match {
+ case Some(o) => {
+ if ( ! accountOwnerExists(o, account)) {
+ MappedAccountHolder.createMappedAccountHolder(o.apiId.value, account.bank, account.id, "KafkaMappedConnector")
+ }
+ }
+ case None => {
+ //This shouldn't happen as OBPUser should generate the APIUsers when saved
+ logger.error(s"api user(s) with email $owner not found.")
+ }
+ }
+ }
+ }
+
+ def updateUserAccountViews( user: APIUser ) = {
+ 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]])
+ }
+
+ val views = for {
+ 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).foreach(v => {
+ Views.views.vend.addPermission(v.uid, user)
+ logger.info(s"------------> updated view ${v.uid} for apiuser ${user} and account ${acc}")
+ })
+ 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}")
+ })
+ }
+ }
+
+ 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
+ }
+
+ 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 =
+ if(owner && ! viewExists(acc, "Owner")) {
+ logger.info("Creating owner view")
+ Some(createSaveableOwnerView(bankId, accountId))
+ }
+ else None
+
+ val publicView =
+ 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 && ! viewExists(acc, "Accountant")) {
+ logger.info("Creating accountants view")
+ Some(createSaveableAccountantsView(bankId, accountId))
+ }
+ else None
+
+ val auditorsView =
+ if(acc.generate_auditors_view && ! viewExists(acc, "Auditor") ) {
+ logger.info("Creating auditors view")
+ Some(createSaveableAuditorsView(bankId, accountId))
+ }
+ else None
+
+ List(ownerView, publicView, accountantsView, auditorsView).flatten
+ }
+
+ //gets banks handled by this connector
+ 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
+ }
+
+ // Gets bank identified by bankId
+ override def getBank(id: BankId): Box[Bank] = {
+ // Generate random uuid to be used as request-respose match id
+ val reqId: String = UUID.randomUUID().toString
+ // Create argument list
+ val argList = Map( "bankId" -> id.toString,
+ "username" -> OBPUser.getCurrentUserUsername )
+ // 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[KafkaInboundBank])
+ }
+ // Return result
+ Full(new KafkaBank(r))
+ }
+
+ // 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-response match id
+ val reqId: String = UUID.randomUUID().toString
+ // Create argument list with reqId
+ // in order to fetch corresponding response
+ val argList = Map( "bankId" -> bankId.toString,
+ "username" -> OBPUser.getCurrentUserUsername,
+ "accountId" -> accountID.toString,
+ "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).extractOpt[KafkaInboundTransaction]
+ r match {
+ // Check does the response data match the requested data
+ case Some(x) if transactionId.value != x.id => Failure(ErrorMessages.InvalidGetTransactionConnectorResponse, Empty, Empty)
+ case Some(x) if transactionId.value == x.id => createNewTransaction(x)
+ case _ => Failure(ErrorMessages.ConnectorEmptyResponse, Empty, Empty)
+ }
+
+ }
+
+ 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 reqId: String = UUID.randomUUID().toString
+ val argList = Map( "bankId" -> bankId.toString,
+ "username" -> OBPUser.getCurrentUserUsername,
+ "accountId" -> accountID.toString,
+ "queryParams" -> queryParams.toString )
+ implicit val formats = net.liftweb.json.DefaultFormats
+ val rList = process(reqId, "getTransactions", argList).extract[List[KafkaInboundTransaction]]
+ // Check does the response data match the requested data
+ val isCorrect = rList.forall(x=>x.this_account.id == accountID.value && x.this_account.bank == bankId.value)
+ if (!isCorrect) throw new Exception(ErrorMessages.InvalidGetTransactionsConnectorResponse)
+ // Populate fields and generate result
+ val res = for {
+ r <- rList
+ transaction <- createNewTransaction(r)
+ } yield {
+ transaction
+ }
+ Full(res)
+ //TODO is this needed updateAccountTransactions(bankId, accountID)
+ }
+
+ 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
+ // in order to fetch corresponding response
+ val argList = Map("bankId" -> bankId.toString,
+ "username" -> OBPUser.getCurrentUserUsername,
+ "accountId" -> accountID.value)
+ // 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[KafkaInboundAccount])
+ }
+ // Check does the response data match the requested data
+ val accResp = List((BankId(r.bank), AccountId(r.id))).toSet
+ val acc = List((bankId, accountID)).toSet
+ if ((accResp diff acc).size > 0) throw new Exception(ErrorMessages.InvalidGetBankAccountConnectorResponse)
+
+ createMappedAccountDataIfNotExisting(r.bank, r.id, r.label)
+
+ Full(new KafkaBankAccount(r))
+ }
+
+ override def getBankAccounts(accts: List[(BankId, AccountId)]): List[KafkaBankAccount] = {
+ // Generate random uuid to be used as request-respose match id
+ val reqId: String = UUID.randomUUID().toString
+ // Create argument list with reqId
+ // in order to fetch corresponding response
+ val argList = Map("bankIds" -> accts.map(a => a._1).mkString(","),
+ "username" -> OBPUser.getCurrentUserUsername,
+ "accountIds" -> accts.map(a => a._2).mkString(","))
+ // 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[KafkaInboundAccount]])
+ }
+ // Check does the response data match the requested data
+ val accRes = for(row <- r) yield {
+ (BankId(row.bank), AccountId(row.id))
+ }
+ if ((accRes.toSet diff accts.toSet).size > 0) throw new Exception(ErrorMessages.InvalidGetBankAccountsConnectorResponse)
+
+ r.map { t =>
+ createMappedAccountDataIfNotExisting(t.bank, t.id, t.label)
+ new KafkaBankAccount(t) }
+ }
+
+ private def getAccountByNumber(bankId : BankId, number : String) : Box[AccountType] = {
+ // Generate random uuid to be used as request-respose match id
+ val reqId: String = UUID.randomUUID().toString
+ // Create argument list with reqId
+ // in order to fetch corresponding response
+ val argList = Map("bankId" -> bankId.toString,
+ "username" -> OBPUser.getCurrentUserUsername,
+ "number" -> number)
+ // 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[KafkaInboundAccount])
+ }
+ createMappedAccountDataIfNotExisting(r.bank, r.id, r.label)
+ Full(new KafkaBankAccount(r))
+ }
+
+ 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)
+ }
+
+ /**
+ *
+ * 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 <- getBank(bankId)
+ account <- getBankAccountType(bankId, accountID)
+ } {
+ spawn{
+ val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false)
+ val outDatedTransactions = Box!!account.lastUpdate match {
+ case Full(l) => now after time(l.getTime + hours(Props.getInt("messageQueue.updateTransactionsInterval", 1)))
+ case _ => true
+ }
+ //if(outDatedTransactions && useMessageQueue) {
+ // UpdatesRequestSender.sendMsg(UpdateBankAccount(account.number, bank.national_identifier.get))
+ //}
+ }
+ }
+ }
+ */
+
+ //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
+
+
+ // 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: AccountType, toAccount: AccountType, 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 : 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 reqId: String = UUID.randomUUID().toString
+ // Create argument list with reqId
+ // in order to fetch corresponding response
+ val argObj = KafkaOutboundTransaction(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 argMap = Extraction.decompose(argObj).values
+ val r = process(reqId, "saveTransaction", argMap.asInstanceOf[Map[String,String]]) //.extract[KafkaInboundTransactionId]
+
+ r.extract[KafkaInboundTransactionId] match {
+ case r: KafkaInboundTransactionId => Full(TransactionId(r.transactionId))
+ case _ => Full(TransactionId("0"))
+ }
+
+ }
+
+ /*
+ Transaction Requests
+ */
+
+ override def createTransactionRequestImpl(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ account : BankAccount, counterparty : BankAccount, body: TransactionRequestBody,
+ status: String, charge: TransactionRequestCharge) : 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 createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ account : BankAccount, details: String,
+ status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = {
+ val mappedTransactionRequest = MappedTransactionRequest210.create
+ .mTransactionRequestId(transactionRequestId.value)
+ .mType(transactionRequestType.value)
+ .mFrom_BankId(account.bankId.value)
+ .mFrom_AccountId(account.accountId.value)
+ .mDetails(details)
+ .mStatus(status)
+ .mStartDate(now)
+ .mEndDate(now).saveMe
+ Full(mappedTransactionRequest).flatMap(_.toTransactionRequest210)
+ }
+
+ 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 getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = {
+ val transactionRequests = MappedTransactionRequest210.findAll(By(MappedTransactionRequest210.mFrom_AccountId, fromAccount.accountId.value),
+ By(MappedTransactionRequest210.mFrom_BankId, fromAccount.bankId.value))
+
+ Full(transactionRequests.flatMap(_.toTransactionRequest210))
+ }
+
+ 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,
+ accountType: String, accountLabel: String, currency: String, accountHolderName: String): (Bank, BankAccount) = {
+ //don't require and exact match on the name, just the identifier
+ val bank: 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,
+ accountType,
+ accountLabel,
+ currency,
+ 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 = {
+ getAccountByNumber(bankId, accountNumber) != null
+ }
+
+ //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 = getBankAccount(bankId, accountID)
+
+ val accountDeleted = account match {
+ case acc => true //acc.delete_! //TODO
+ 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,
+ accountType: String, accountLabel: 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, accountType, accountLabel, currency, balanceInSmallestCurrencyUnits, accountHolderName)
+ }
+
+ }
+
+ //sets a user as an account owner/holder
+ override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = {
+ MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankAccountUID.accountId.value, bankAccountUID.bankId.value)
+ }
+
+ private def createAccountIfNotExisting(bankId: BankId, accountID: AccountId, accountNumber: String,
+ accountType: String, accountLabel: String, currency: String,
+ balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = {
+ 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
+ case _ => null //TODO
+ /*
+ new KafkaBankAccount
+ .bank(bankId.value)
+ .theAccountId(accountID.value)
+ .accountNumber(accountNumber)
+ .accountType(accountType)
+ .accountLabel(accountLabel)
+ .accountCurrency(currency)
+ .accountBalance(balanceInSmallestCurrencyUnits)
+ .holder(accountHolderName)
+ .saveMe()
+ */
+ }
+ }
+
+ private def createMappedAccountDataIfNotExisting(bankId: String, accountId: String, label: String) : Boolean = {
+ MappedKafkaBankAccountData.find(By(MappedKafkaBankAccountData.accountId, accountId),
+ By(MappedKafkaBankAccountData.bankId, bankId)) match {
+ case Empty =>
+ val data = new MappedKafkaBankAccountData
+ data.setAccountId(accountId)
+ data.setBankId(bankId)
+ data.setLabel(label)
+ data.save()
+ true
+ case _ =>
+ logger.info(s"account data with id $accountId at bank with id $bankId already exists. No need to create a new one.")
+ false
+ }
+ }
+
+ /*
+ End of bank account creation
+ */
+
+
+ /*
+ 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 <- getBankAccount(bankId, accountID)
+ bank <- getBank(bankId)
+ } yield {
+ //acc.balance = newBalance
+ 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 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 = 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
+ }
+ }
+ 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 <- getBankAccount(bankId, accountID)
+ bank <- getBank(bankId)
+ d <- MappedKafkaBankAccountData.find(By(MappedKafkaBankAccountData.accountId, accountID.value), By(MappedKafkaBankAccountData.bankId, bank.bankId.value))
+ } yield {
+ d.setLabel(label)
+ d.save()
+ }
+ result.getOrElse(false)
+ }
+
+
+
+
+ /////////////////////////////////////////////////////////////////////////////
+
+
+
+ def process(reqId: String, command: String, argList: Map[String,String]): json.JValue = { //List[Map[String,String]] = {
+ if (producer.send(reqId, command, argList, "1")) {
+ // Request sent, now we wait for response with the same reqId
+ val res = consumer.getResponse(reqId)
+ return res
+ }
+ return json.parse("""{"error":"could not send message to kafka"}""")
+ }
+
+
+ // Helper for creating a transaction
+ 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)
+
+ var dateCompleted: Date = null
+ 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)
+
+ for {
+ 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.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.get, 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)
+ )
+ }
+ }
+
+
+ case class KafkaBank(r: KafkaInboundBank) extends Bank {
+ def fullName = r.full_name
+ def shortName = r.short_name
+ def logoUrl = r.logo
+ def bankId = BankId(r.id)
+ def nationalIdentifier = "None" //TODO
+ def swiftBic = "None" //TODO
+ def websiteUrl = r.website
+ }
+
+ // Helper for creating other bank account
+ def createOtherBankAccount(c: KafkaInboundTransactionCounterparty, o: KafkaBankAccount, alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = {
+ new OtherBankAccount(
+ id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""),
+ label = c.account_number.getOrElse(c.name.getOrElse("")),
+ nationalIdentifier = "",
+ swift_bic = None,
+ iban = None,
+ number = c.account_number.getOrElse(""),
+ bankName = "",
+ kind = "",
+ originalPartyBankId = BankId(o.bankId.value),
+ originalPartyAccountId = AccountId(o.accountId.value),
+ alreadyFoundMetadata = alreadyFoundMetadata
+ )
+ }
+
+ case class KafkaBankAccount(r: KafkaInboundAccount) extends BankAccount {
+ def accountId : AccountId = AccountId(r.id)
+ def accountType : String = r.`type`
+ def balance : BigDecimal = BigDecimal(r.balance.amount)
+ def currency : String = r.balance.currency
+ def name : String = r.owners.head
+ def swift_bic : Option[String] = Some("swift_bic") //TODO
+ def iban : Option[String] = Some(r.IBAN)
+ def number : String = r.number
+ def bankId : BankId = BankId(r.bank)
+ def lastUpdate : Date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).parse(today.getTime.toString)
+ def accountHolder : String = r.owners.head
+
+ // Fields modifiable from OBP are stored in mapper
+ def label : String = (for {
+ d <- MappedKafkaBankAccountData.find(By(MappedKafkaBankAccountData.accountId, r.id))
+ } yield {
+ d.getLabel
+ }).getOrElse(r.number)
+
+ }
+
+
+ case class KafkaInboundBank(
+ id : String,
+ short_name : String,
+ full_name : String,
+ logo : String,
+ website : String)
+
+
+ /** 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 KafkaInboundLicense(
+ id : String,
+ name : String)
+
+ case class KafkaInboundMeta(
+ license : KafkaInboundLicense)
+
+ case class KafkaInboundLobby(
+ hours : String)
+
+ case class KafkaInboundDriveUp(
+ hours : String)
+
+ /**
+ *
+ * @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,
+ city : String,
+ county : String, // Division of State
+ state : String, // Division of Country
+ post_code : String,
+ country_code: String)
+
+ case class KafkaInboundLocation(
+ latitude : Double,
+ longitude : Double)
+
+ case class KafkaInboundUser(
+ email : String,
+ password : String,
+ display_name : String)
+
+ case class KafkaInboundValidatedUser(
+ email : String,
+ display_name : String)
+
+ 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 KafkaInboundBalance(
+ currency : String,
+ amount : String)
+
+ case class KafkaInboundTransaction(
+ id : String,
+ this_account : KafkaInboundAccountId,
+ counterparty : Option[KafkaInboundTransactionCounterparty],
+ details : KafkaInboundTransactionDetails)
+
+ case class KafkaInboundTransactionCounterparty(
+ name : Option[String], // Also known as Label
+ account_number : Option[String])
+
+ case class KafkaInboundAccountId(
+ id : String,
+ bank : String)
+
+ case class KafkaInboundTransactionDetails(
+ `type` : String,
+ description : String,
+ posted : String,
+ completed : String,
+ new_balance : String,
+ value : String)
+
+
+ case class KafkaInboundAtm(
+ id : String,
+ bank_id: String,
+ name : String,
+ address : KafkaInboundAddress,
+ location : KafkaInboundLocation,
+ meta : KafkaInboundMeta
+ )
+
+
+ case class KafkaInboundProduct(
+ bank_id : String,
+ code: String,
+ name : String,
+ category : String,
+ family : String,
+ super_family : String,
+ more_info_url : String,
+ meta : KafkaInboundMeta
+ )
+
+
+ case class KafkaInboundAccountData(
+ banks : List[KafkaInboundBank],
+ users : List[KafkaInboundUser],
+ accounts : List[KafkaInboundAccount]
+ )
+
+ // We won't need this. TODO clean up.
+ 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 KafkaInboundCrmEvent(
+ id : String, // crmEventId
+ bank_id : String,
+ customer: KafkaInboundCustomer,
+ category : String,
+ detail : String,
+ channel : String,
+ actual_date: String
+ )
+
+ case class KafkaInboundCustomer(
+ name: String,
+ number : String // customer number, also known as ownerId (owner of accounts) aka API User?
+ )
+
+
+ case class KafkaInboundTransactionId(
+ transactionId : String
+ )
+ case class KafkaOutboundTransaction(username: String,
+ accountId: String,
+ currency: String,
+ amount: String,
+ otherAccountId: String,
+ otherAccountCurrency: String,
+ transactionType: String)
+}
+
diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala
index 4162b2954..df4663ef2 100644
--- a/src/main/scala/code/bankconnectors/LocalConnector.scala
+++ b/src/main/scala/code/bankconnectors/LocalConnector.scala
@@ -1,20 +1,27 @@
package code.bankconnectors
import java.text.SimpleDateFormat
-import java.util.TimeZone
-import net.liftweb.common.{Failure, Box, Loggable, Full}
-import net.liftweb.json.JsonAST.JValue
-import scala.concurrent.ops.spawn
+import java.util.{Date, TimeZone, UUID}
+
+import code.management.ImporterAPI.ImporterTransaction
+import code.metadata.counterparties.{Counterparties, Metadata, MongoCounterparties}
import code.model._
import code.model.dataAccess._
+import code.transactionrequests.TransactionRequests._
+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.UpdateBankAccount
+import org.bson.types.ObjectId
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent._
private object LocalConnector extends Connector with Loggable {
@@ -27,7 +34,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 getBankAccount(bankId : BankId, accountId : AccountId) : Box[Account] = {
for{
bank <- getHostedBank(bankId)
account <- bank.getAccount(accountId)
@@ -131,15 +138,15 @@ private object LocalConnector extends Connector with Loggable {
By(MappedAccountHolder.accountPermalink, accountID.value)).map(accHolder => accHolder.user.obj).flatten.toSet
}
- override protected def makePaymentImpl(fromAccount : Account, toAccount : Account, amt : BigDecimal) : Box[TransactionId] = {
+ override protected def makePaymentImpl(fromAccount : Account, toAccount : Account, amt : BigDecimal, description: String) : Box[TransactionId] = {
val fromTransAmt = -amt //from account balance should decrease
val toTransAmt = amt //to account balance should increase
//this is the transaction that gets attached to the account of the person making the payment
- val createdFromTrans = saveNewTransaction(fromAccount, toAccount, fromTransAmt)
+ val createdFromTrans = saveNewTransaction(fromAccount, toAccount, fromTransAmt, description)
// this creates the transaction that gets attached to the account of the person receiving the payment
- saveNewTransaction(toAccount, fromAccount, toTransAmt)
+ saveNewTransaction(toAccount, fromAccount, toTransAmt, description)
//assumes OBPEnvelope id is what gets used as the Transaction id in the API. If that gets changed, this needs to
//be updated (the tests should fail if it doesn't)
@@ -209,7 +216,7 @@ private object LocalConnector extends Connector with Loggable {
balance)
}
- private def saveNewTransaction(account : Account, otherAccount : Account, amount : BigDecimal) : Box[OBPEnvelope] = {
+ private def saveNewTransaction(account : Account, otherAccount : Account, amount : BigDecimal, description : String) : Box[OBPEnvelope] = {
val oldBalance = account.balance
@@ -239,7 +246,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) ~
@@ -270,7 +277,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
@@ -287,9 +294,9 @@ 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.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))
}
@@ -297,6 +304,31 @@ private object LocalConnector extends Connector with Loggable {
}
+ /*
+ Transaction Requests
+ */
+
+ override def createTransactionRequestImpl(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ account : BankAccount, counterparty : BankAccount, body: TransactionRequestBody,
+ status: String, charge: TransactionRequestCharge) : Box[TransactionRequest] = ???
+
+ override def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ account : BankAccount, details: String,
+ status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = ???
+
+ override def saveTransactionRequestTransactionImpl(transactionRequestId: TransactionRequestId, transactionId: TransactionId) = ???
+ override def saveTransactionRequestChallengeImpl(transactionRequestId: TransactionRequestId, challenge: TransactionRequestChallenge) = ???
+ override def getTransactionRequestsImpl(fromAccount : BankAccount) : Box[List[TransactionRequest]] = ???
+ override def getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = ???
+ override def getTransactionRequestImpl(transactionRequestId: TransactionRequestId) : Box[TransactionRequest] = ???
+ override def getTransactionRequestTypesImpl(fromAccount : BankAccount) : Box[List[TransactionRequestType]] = {
+ //TODO: write logic / data access
+ Full(List(TransactionRequestType("SANDBOX_TAN")))
+ }
+ override def saveTransactionRequestStatusImpl(transactionRequestId: TransactionRequestId, status: String) = ???
+
+
+
private def createOtherBankAccount(originalPartyBankId: BankId, originalPartyAccountId: AccountId,
otherAccount : OtherBankAccountMetadata, otherAccountFromTransaction : OBPAccount) : OtherBankAccount = {
new OtherBankAccount(
@@ -317,4 +349,206 @@ private object LocalConnector extends Connector with Loggable {
private def getHostedBank(bankId : BankId) : Box[HostedBank] = {
HostedBank.find("permalink", bankId.value) ?~ {"bank " + bankId + " not found"}
}
+
+ //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,
+ accountType: String, accountLabel: String, currency : String, initialBalance : BigDecimal, holderName : String) : BankAccount = {
+ import net.liftweb.mongodb.BsonDSL._
+ Account.find(
+ (Account.accountNumber.name -> accountNumber)~
+ (Account.bankID.name -> hostedBank.id.is)
+ ) match {
+ case Full(bankAccount) => {
+ logger.info(s"account with number ${bankAccount.accountNumber} at bank ${hostedBank.bankId} already exists. No need to create a new one.")
+ bankAccount
+ }
+ case _ => {
+ logger.info("creating account record ")
+ val bankAccount =
+ Account
+ .createRecord
+ .accountBalance(initialBalance)
+ .holder(holderName)
+ .accountNumber(accountNumber)
+ .kind(accountType)
+ .accountLabel(accountLabel)
+ .accountName("")
+ .permalink(accountId.value)
+ .bankID(hostedBank.id.is)
+ .accountCurrency(currency)
+ .accountIban("")
+ .accountLastUpdate(now)
+ .save
+ bankAccount
+ }
+ }
+ }
+
+ //creates a bank account (if it doesn't exist) and creates a bank (if it doesn't exist)
+ override def createBankAndAccount(bankName : String, bankNationalIdentifier : String, accountNumber : String, accountType: String, accountLabel: String, currency: String, accountHolderName : String): (Bank, BankAccount) = {
+
+ // TODO: use a more unique id for the long term
+ val hostedBank = {
+ // TODO: use a more unique id for the long term
+ HostedBank.find(HostedBank.national_identifier.name, bankNationalIdentifier) match {
+ case Full(b)=> {
+ logger.info(s"bank ${b.name} found")
+ b
+ }
+ case _ =>{
+ //TODO: if name is empty use bank id as name alias
+
+ //TODO: need to handle the case where generatePermalink returns a permalink that is already used for another bank
+
+ logger.info(s"creating HostedBank")
+ HostedBank
+ .createRecord
+ .name(bankName)
+ .alias(bankName)
+ .permalink(Helper.generatePermalink(bankName))
+ .national_identifier(bankNationalIdentifier)
+ .save
+ }
+ }
+ }
+
+ val createdAccount = createAccount(hostedBank, AccountId(UUID.randomUUID().toString),
+ accountNumber, accountType, accountLabel, currency, BigDecimal("0.00"), accountHolderName)
+
+ (hostedBank, createdAccount)
+ }
+
+ //sets a user as an account owner/holder
+ override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = {
+ 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
+ override def accountExists(bankId: BankId, accountNumber: String): Boolean = {
+ import net.liftweb.mongodb.BsonDSL._
+
+ getHostedBank(bankId).map(_.id.get) match {
+ case Full(mongoId) =>
+ Account.count((Account.accountNumber.name -> accountNumber) ~ (Account.bankID.name -> mongoId)) > 0
+ case _ =>
+ logger.warn("tried to check account existence for an account at a bank that doesn't exist")
+ false
+ }
+ }
+
+ 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,
+ accountType: String, accountLabel: String, currency: String,
+ initialBalance: BigDecimal, accountHolderName: String): Box[BankAccount] = {
+ HostedBank.find(bankId) match {
+ case Full(b) => Full(createAccount(b, accountId, accountNumber, accountType, accountLabel, currency, initialBalance, accountHolderName))
+ case _ => Failure(s"Bank with id ${bankId.value} not found. Cannot create account at non-existing bank.")
+ }
+
+ }
+
+ //used by transaction import api call to check for duplicates
+ override def getMatchingTransactionCount(bankNationalIdentifier : String, accountNumber : String, amount: String, completed: Date, otherAccountHolder: String): Int = {
+
+ val baseQuery = QueryBuilder.start("obp_transaction.details.value.amount")
+ .is(amount)
+ .put("obp_transaction.details.completed")
+ .is(completed)
+ .put("obp_transaction.this_account.bank.national_identifier")
+ .is(bankNationalIdentifier)
+ .put("obp_transaction.this_account.number")
+ .is(accountNumber)
+
+ //this is refactored legacy code, and it seems the empty account holder check had to do with potentially missing
+ //fields in the db. not sure if this is still required.
+ if(otherAccountHolder.isEmpty){
+ def emptyHolderOrEmptyString(holder: Box[String]): Boolean = {
+ holder match {
+ case Full(s) => s.isEmpty
+ case _ => true
+ }
+ }
+
+ val partialMatches = OBPEnvelope.findAll(baseQuery.get())
+
+ partialMatches.filter(e => {
+ emptyHolderOrEmptyString(e.obp_transaction.get.other_account.get.holder.valueBox)
+ }).size
+ }
+ else{
+ val qry = baseQuery.put("obp_transaction.other_account.holder").is(otherAccountHolder).get
+
+ val partialMatches = OBPEnvelope.count(qry)
+ partialMatches.toInt //icky
+ }
+ }
+
+ //used by transaction import api
+ override def createImportedTransaction(transaction: ImporterTransaction): Box[Transaction] = {
+ import net.liftweb.mongodb.BsonDSL._
+
+ implicit val formats = net.liftweb.json.DefaultFormats.lossless
+ val asJValue = Extraction.decompose(transaction)
+
+ for {
+ env <- OBPEnvelope.envelopesFromJValue(asJValue)
+ nationalIdentifier = transaction.obp_transaction.this_account.bank.national_identifier
+ bank <- HostedBank.find(HostedBank.national_identifier.name -> nationalIdentifier) ?~
+ s"No bank found with national identifier ${nationalIdentifier} could not save envelope"
+ accountNumber = transaction.obp_transaction.this_account.number
+ account <- Account.find((Account.bankID.name -> bank.id.get) ~ (Account.accountNumber.name -> accountNumber)) ?~
+ s"No account found with number ${accountNumber} at bank with id ${bank.bankId}: could not save envelope"
+ savedEnv <- env.saveTheRecord() ?~ "Could not save envelope"
+ } yield {
+ createTransaction(savedEnv, account)
+ }
+ }
+
+ //used by the transaction import api
+ override def updateAccountBalance(bankId: BankId, accountId: AccountId, newBalance: BigDecimal): Boolean = {
+ getBankAccount(bankId, accountId) match {
+ case Full(acc) =>
+ acc.accountBalance(newBalance).saveTheRecord().isDefined
+ true
+ case _ =>
+ 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
+ }
+ }
+
+ override def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean = {
+ getBankAccount(bankId, accountId) match {
+ case Full(acc) =>
+ acc.accountLabel(label).saveTheRecord().isDefined
+ true
+ case _ =>
+ 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 db0d24d4a..5182d1b54 100644
--- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
+++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
@@ -1,15 +1,30 @@
package code.bankconnectors
+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._
-import code.model.dataAccess.{UpdatesRequestSender, MappedBankAccount, MappedAccountHolder, MappedBank}
+import code.model.dataAccess._
+import code.tesobe.CashTransaction
+import code.transaction.MappedTransaction
+import code.transactionrequests.{MappedTransactionRequest210, MappedTransactionRequest}
+import code.transactionrequests.TransactionRequests._
import code.util.Helper
import com.tesobe.model.UpdateBankAccount
-import net.liftweb.common.{Loggable, Full, Box}
+import net.liftweb.common.{Box, Failure, Full, Loggable}
import net.liftweb.mapper._
import net.liftweb.util.Helpers._
import net.liftweb.util.Props
-import scala.concurrent.ops._
+
+import scala.concurrent._
+import scala.concurrent.ExecutionContext.Implicits.global
object LocalMappedConnector extends Connector with Loggable {
@@ -76,11 +91,14 @@ object LocalMappedConnector extends Connector with Loggable {
for {
bank <- getMappedBank(bankId)
- account <- getBankAccountType(bankId, accountId)
+ account <- getBankAccount(bankId, accountId)
} {
- spawn{
+ Future{
val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false)
- val outDatedTransactions = now after time(account.lastUpdate.get.getTime + hours(1))
+ 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))
}
@@ -88,7 +106,8 @@ object LocalMappedConnector extends Connector with Loggable {
}
}
- override def getBankAccountType(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = {
+ // Question: Why is this called getBankAccountType? Why not getBankAccount? TODO rename
+ override def getBankAccount(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = {
MappedBankAccount.find(
By(MappedBankAccount.bank, bankId.value),
By(MappedBankAccount.theAccountId, accountId.value))
@@ -128,10 +147,13 @@ object LocalMappedConnector extends Connector with Loggable {
}
}
+ // 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] =
@@ -141,14 +163,29 @@ object LocalMappedConnector extends Connector with Loggable {
Set.empty
- override def makePaymentImpl(fromAccount: MappedBankAccount, toAccount: MappedBankAccount, amt: BigDecimal): Box[TransactionId] = {
- val fromTransAmt = -amt //from account balance should decrease
- val toTransAmt = amt //to account balance should increase
+ override def makePaymentImpl(fromAccount: MappedBankAccount, toAccount: MappedBankAccount, amt: BigDecimal, description : String): Box[TransactionId] = {
//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)
- saveTransaction(toAccount, fromAccount, toTransAmt)
+
+
+ val rate = tryo {
+ fx.exchangeRate(fromAccount.currency, toAccount.currency)
+ } ?~! {
+ s"The requested currency conversion (${fromAccount.currency} to ${toAccount.currency}) is not supported."
+ }
+
+ // Is it better to pass these into this function ?
+ val fromTransAmt = -amt //from account balance should decrease
+ val toTransAmt = fx.convert(amt, rate.get)
+
+ // From
+ val sentTransactionId = saveTransaction(fromAccount, toAccount, fromTransAmt, description)
+
+ // To
+ val recievedTransactionId = saveTransaction(toAccount, fromAccount, toTransAmt, description)
+
+ // Return the sent transaction id
sentTransactionId
}
@@ -156,7 +193,7 @@ object LocalMappedConnector extends Connector 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 : MappedBankAccount, counterparty : BankAccount, amt : BigDecimal) : Box[TransactionId] = {
+ private def saveTransaction(account : MappedBankAccount, counterparty : BankAccount, amt : BigDecimal, description : String) : Box[TransactionId] = {
val transactionTime = now
val currency = account.currency
@@ -166,7 +203,6 @@ object LocalMappedConnector extends Connector with Loggable {
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)
@@ -176,7 +212,7 @@ object LocalMappedConnector extends Connector with Loggable {
.currency(currency)
.tStartDate(transactionTime)
.tFinishDate(transactionTime)
- .description("")
+ .description(description)
.counterpartyAccountHolder(counterparty.accountHolder)
.counterpartyAccountNumber(counterparty.number)
.counterpartyAccountKind(counterparty.accountType)
@@ -187,4 +223,394 @@ object LocalMappedConnector extends Connector with Loggable {
Full(mappedTransaction.theTransactionId)
}
+ /*
+ Transaction Requests
+ */
+
+ override def createTransactionRequestImpl(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ account : BankAccount, counterparty : BankAccount, body: TransactionRequestBody,
+ status: String, charge: TransactionRequestCharge) : 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)
+ .mCharge_Summary(charge.summary)
+ .mCharge_Amount(charge.value.amount)
+ .mCharge_Currency(charge.value.currency)
+ .saveMe
+ Full(mappedTransactionRequest).flatMap(_.toTransactionRequest)
+ }
+
+ override def createTransactionRequestImpl210(transactionRequestId: TransactionRequestId, transactionRequestType: TransactionRequestType,
+ account : BankAccount, details: String,
+ status: String, charge: TransactionRequestCharge) : Box[TransactionRequest210] = {
+ val mappedTransactionRequest = MappedTransactionRequest210.create
+ .mTransactionRequestId(transactionRequestId.value)
+ .mType(transactionRequestType.value)
+ .mFrom_BankId(account.bankId.value)
+ .mFrom_AccountId(account.accountId.value)
+ .mDetails(details)
+ .mStatus(status)
+ .mStartDate(now)
+ .mEndDate(now)
+ .mCharge_Summary(charge.summary)
+ .mCharge_Amount(charge.value.amount)
+ .mCharge_Currency(charge.value.currency)
+ .saveMe
+ Full(mappedTransactionRequest).flatMap(_.toTransactionRequest210)
+ }
+
+ 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 getTransactionRequestsImpl210(fromAccount : BankAccount) : Box[List[TransactionRequest210]] = {
+ val transactionRequests = MappedTransactionRequest210.findAll(By(MappedTransactionRequest210.mFrom_AccountId, fromAccount.accountId.value),
+ By(MappedTransactionRequest210.mFrom_BankId, fromAccount.bankId.value))
+
+ Full(transactionRequests.flatMap(_.toTransactionRequest210))
+ }
+
+ 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
+ // Get Transaction Request Types from Props "transactionRequests_supported_types". Default is empty string
+ val validTransactionRequestTypes = Props.get("transactionRequests_supported_types", "").split(",").map(x => TransactionRequestType(x)).toList
+ Full(validTransactionRequestTypes)
+ }
+
+ /*
+ 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, accountType: String, accountLabel: String, currency: 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, accountType, accountLabel, currency, 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,
+ accountType: String, accountLabel: 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, accountType, accountLabel, currency, balanceInSmallestCurrencyUnits, accountHolderName)
+ }
+
+ }
+
+ //sets a user as an account owner/holder
+ override def setAccountHolder(bankAccountUID: BankAccountUID, user: User): Unit = {
+ MappedAccountHolder.createMappedAccountHolder(user.apiId.value, bankAccountUID.bankId.value, bankAccountUID.accountId.value)
+ }
+
+ private def createAccountIfNotExisting(bankId: BankId, accountId: AccountId, accountNumber: String,
+ accountType: String, accountLabel: String, currency: String,
+ balanceInSmallestCurrencyUnits: Long, accountHolderName: String) : BankAccount = {
+ 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
+ case _ =>
+ MappedBankAccount.create
+ .bank(bankId.value)
+ .theAccountId(accountId.value)
+ .accountNumber(accountNumber)
+ .kind(accountType)
+ .accountLabel(accountLabel)
+ .accountCurrency(currency)
+ .accountBalance(balanceInSmallestCurrencyUnits)
+ .holder(accountHolderName)
+ .saveMe()
+ }
+ }
+
+ /*
+ End of bank account creation
+ */
+
+
+ /*
+ 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 <- getBankAccount(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 <- getBankAccount(bankId, accountId)
+ bank <- getMappedBank(bankId)
+ } yield {
+ acc.accountLabel(label).save
+ }
+
+ result.getOrElse(false)
+ }
+
}
diff --git a/src/main/scala/code/branches/Branches.scala b/src/main/scala/code/branches/Branches.scala
new file mode 100644
index 000000000..e71cedd7a
--- /dev/null
+++ b/src/main/scala/code/branches/Branches.scala
@@ -0,0 +1,90 @@
+package code.branches
+
+
+/* For branches */
+
+// 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 Branches extends SimpleInjector {
+
+ case class BranchId(value : String)
+
+ trait Branch {
+ def branchId : BranchId
+ def name : String
+ def address : Address
+ def location : Location
+ def lobby : Lobby
+ def driveUp : DriveUp
+ def meta : Meta
+ }
+
+ trait Lobby {
+ def hours : String
+ }
+
+ trait DriveUp {
+ def hours : String
+ }
+
+ 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 {
+
+ private val logger = Logger(classOf[BranchesProvider])
+
+
+ /*
+ Common logic for returning branches.
+ Implementation details in branchesData
+ */
+ final def getBranches(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 getBranch(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
+}
+
diff --git a/src/main/scala/code/branches/MappedBranchesProvider.scala b/src/main/scala/code/branches/MappedBranchesProvider.scala
new file mode 100644
index 000000000..d6fe99f46
--- /dev/null
+++ b/src/main/scala/code/branches/MappedBranchesProvider.scala
@@ -0,0 +1,117 @@
+package code.branches
+
+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 MappedBranchesProvider extends BranchesProvider {
+
+ override protected def getBranchFromProvider(branchId: BranchId): Option[Branch] =
+ MappedBranch.find(By(MappedBranch.mBranchId, branchId.value))
+
+ override protected def getBranchesFromProvider(bankId: BankId): Option[List[Branch]] = {
+ Some(MappedBranch.findAll(By(MappedBranch.mBankId, bankId.value)))
+ }
+}
+
+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 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 MappedBranch extends MappedBranch with LongKeyedMetaMapper[MappedBranch] {
+ 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/common/Common.scala b/src/main/scala/code/common/Common.scala
new file mode 100644
index 000000000..12326cef6
--- /dev/null
+++ b/src/main/scala/code/common/Common.scala
@@ -0,0 +1,36 @@
+package code.common
+
+trait License {
+ def id : String
+ def name : String
+ }
+
+ trait Meta {
+ def license : License
+ }
+
+ trait Address {
+ def line1 : String
+ def line2 : String
+ def line3 : String
+ def city : String
+ def county : String
+ def state : String
+ def postCode : String
+ //ISO_3166-1_alpha-2
+ def countryCode : String
+ }
+
+ trait Location {
+ def latitude: Double
+ def longitude: Double
+ }
+
+
+
+
+
+
+
+
+
diff --git a/src/main/scala/code/crm/CrmEvent.scala b/src/main/scala/code/crm/CrmEvent.scala
new file mode 100644
index 000000000..e9368901d
--- /dev/null
+++ b/src/main/scala/code/crm/CrmEvent.scala
@@ -0,0 +1,103 @@
+package code.crm
+
+/* For crmEvents */
+
+
+
+import code.crm.CrmEvent.{CrmEvent, CrmEventId}
+import code.model.BankId
+import code.common.{Address, Location, Meta}
+import code.model.dataAccess.APIUser
+import code.model.dataAccess.APIUser
+import net.liftweb.common.Logger
+import net.liftweb.util
+import net.liftweb.util.SimpleInjector
+
+import java.util.Date
+
+object CrmEvent extends util.SimpleInjector {
+
+ case class CrmEventId(value : String)
+
+
+ trait CrmEvent {
+ def crmEventId: CrmEventId
+ def bankId: BankId
+ def user: APIUser
+ def customerName : String
+ def customerNumber : String // Is this duplicate of APIUser?
+ def category : String
+ def detail : String
+ def channel : String
+ def scheduledDate : Date
+ def actualDate: Date
+ def result: String}
+
+ val crmEventProvider = new Inject(buildOne _) {}
+
+ def buildOne: CrmEventProvider = MappedCrmEventProvider
+
+ // Helper to get the count out of an option
+ def countOfCrmEvents (listOpt: Option[List[CrmEvent]]) : Int = {
+ val count = listOpt match {
+ case Some(list) => list.size
+ case None => 0
+ }
+ count
+ }
+
+
+}
+
+trait CrmEventProvider {
+
+ private val logger = Logger(classOf[CrmEventProvider])
+
+
+ /*
+ Common logic for returning all crmEvents at a bank
+ */
+ final def getCrmEvents(bankId : BankId) : Option[List[CrmEvent]] = {
+ // If we get crmEvents filter them
+ getEventsFromProvider(bankId) match {
+ case Some(allItems) => {
+ val returnItems = for {
+ item <- allItems // No filtering required
+ } yield item
+ Option(returnItems)
+ }
+ case None => None
+ }
+ }
+
+ /*
+ Common logic for returning crmEvents at a bank for one user
+ */
+ final def getCrmEvents(bankId : BankId, user : APIUser) : Option[List[CrmEvent]] = {
+ getEventsFromProvider(bankId, user) // No filter required
+ }
+
+ /*
+ Common logic for returning one crmEvent
+ */
+ final def getCrmEvent(crmEventId: CrmEventId) : Option[CrmEvent] = {
+ getEventFromProvider(crmEventId) // No filter required
+ }
+
+
+
+
+ // For the whole bank
+ protected def getEventsFromProvider(bank : BankId) : Option[List[CrmEvent]]
+
+ // For a user
+ protected def getEventsFromProvider(bank : BankId, user : APIUser) : Option[List[CrmEvent]]
+
+ // One event
+ protected def getEventFromProvider(crmEventId: CrmEventId) : Option[CrmEvent]
+
+
+
+
+ // End of Trait
+}
diff --git a/src/main/scala/code/crm/MappedCrmEventProvider.scala b/src/main/scala/code/crm/MappedCrmEventProvider.scala
new file mode 100644
index 000000000..f119da2a2
--- /dev/null
+++ b/src/main/scala/code/crm/MappedCrmEventProvider.scala
@@ -0,0 +1,82 @@
+package code.crm
+
+import java.util.Date
+
+import code.crm.CrmEvent._
+import code.crm.CrmEvent.{CrmEvent, CrmEventId}
+
+import code.customer.CustomerMessage
+import code.model.BankId
+
+import code.common.{Address, License, Location, Meta}
+import code.model.dataAccess.APIUser
+
+import code.util.{MappedUUID, DefaultStringField}
+import net.liftweb.common.Box
+import net.liftweb.mapper._
+import org.joda.time.Hours
+
+import scala.util.Try
+
+object MappedCrmEventProvider extends CrmEventProvider {
+
+ // Get all events at a bank
+ override protected def getEventsFromProvider(bankId: BankId): Option[List[CrmEvent]] = {
+ Some(MappedCrmEvent.findAll(
+ By(MappedCrmEvent.mBankId, bankId.value)
+ )
+ )
+ }
+
+ // Get events at a bank for one user
+ override protected def getEventsFromProvider(bankId: BankId, user: APIUser): Option[List[CrmEvent]] =
+ Some(MappedCrmEvent.findAll(
+ By(MappedCrmEvent.mBankId, bankId.toString),
+ By(MappedCrmEvent.mUserId, user)
+ )
+ )
+
+
+ override protected def getEventFromProvider(crmEventId: CrmEventId): Option[CrmEvent] =
+ MappedCrmEvent.find(
+ By(MappedCrmEvent.mCrmEventId, crmEventId.value)
+ )
+
+
+}
+
+
+class MappedCrmEvent extends CrmEvent with LongKeyedMapper[MappedCrmEvent] with IdPK with CreatedUpdated {
+
+ override def getSingleton = MappedCrmEvent
+
+ object mBankId extends DefaultStringField(this) // Should be a foreign key
+ object mUserId extends MappedLongForeignKey(this, APIUser) // The customer
+ object mCrmEventId extends MappedUUID(this)
+ object mCategory extends DefaultStringField(this)
+ object mDetail extends DefaultStringField(this)
+ object mChannel extends DefaultStringField(this)
+ object mScheduledDate extends MappedDateTime(this)
+ object mActualDate extends MappedDateTime(this)
+ object mResult extends DefaultStringField(this)
+ object mCustomerName extends DefaultStringField(this)
+ object mCustomerNumber extends DefaultStringField(this) // Same as api user id?
+
+ override def bankId: BankId = BankId(mBankId.get)
+ override def crmEventId: CrmEventId = CrmEventId(mCrmEventId.get)
+ override def category: String = mCategory.get
+ override def detail: String = mDetail.get
+ override def channel: String = mChannel.get
+ override def scheduledDate: Date = mScheduledDate.get
+ override def actualDate: Date = mActualDate.get
+ override def result: String = mResult.get
+ override def user: APIUser = mUserId.obj.get
+ override def customerName : String = mCustomerName.get
+ override def customerNumber : String = mCustomerNumber.get
+}
+
+object MappedCrmEvent extends MappedCrmEvent with LongKeyedMetaMapper[MappedCrmEvent] {
+ // Note: Makes sense for event id to be unique in system
+ override def dbIndexes = UniqueIndex(mCrmEventId) :: Index(mBankId) :: super.dbIndexes
+}
+
diff --git a/src/main/scala/code/customerinfo/CustomerMessage.scala b/src/main/scala/code/customer/CustomerMessage.scala
similarity index 96%
rename from src/main/scala/code/customerinfo/CustomerMessage.scala
rename to src/main/scala/code/customer/CustomerMessage.scala
index e476447b2..b3e802536 100644
--- a/src/main/scala/code/customerinfo/CustomerMessage.scala
+++ b/src/main/scala/code/customer/CustomerMessage.scala
@@ -1,4 +1,4 @@
-package code.customerinfo
+package code.customer
import java.util.Date
diff --git a/src/main/scala/code/customer/CustomerProvider.scala b/src/main/scala/code/customer/CustomerProvider.scala
new file mode 100644
index 000000000..b7a2ea051
--- /dev/null
+++ b/src/main/scala/code/customer/CustomerProvider.scala
@@ -0,0 +1,65 @@
+package code.customer
+
+import java.util.Date
+
+import code.model.{BankId, User}
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+object Customer extends SimpleInjector {
+
+ val customerProvider = new Inject(buildOne _) {}
+
+ def buildOne: CustomerProvider = MappedCustomerProvider
+
+}
+
+trait CustomerProvider {
+ def getCustomer(bankId : BankId, user : User) : Box[Customer]
+
+ 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]
+
+ 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,
+ relationshipStatus: String,
+ dependents: Int,
+ dobOfDependents: List[Date],
+ highestEducationAttained: String,
+ employmentStatus: String,
+ kycStatus: Boolean,
+ lastOkDate: Date): Box[Customer]
+
+}
+
+trait Customer {
+ 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
+ def faceImage : CustomerFaceImage
+ def dateOfBirth: Date
+ def relationshipStatus: String
+ def dependents: Int
+ def dobOfDependents: List[Date]
+ def highestEducationAttained: String
+ def employmentStatus: String
+ def kycStatus: Boolean
+ def lastOkDate: Date
+}
+
+trait CustomerFaceImage {
+ def url : String
+ def date : Date
+}
+
+case class MockCustomerFaceImage(date : Date, url : String) extends CustomerFaceImage
\ No newline at end of file
diff --git a/src/main/scala/code/customerinfo/MappedCustomerMessageProvider.scala b/src/main/scala/code/customer/MappedCustomerMessageProvider.scala
similarity index 98%
rename from src/main/scala/code/customerinfo/MappedCustomerMessageProvider.scala
rename to src/main/scala/code/customer/MappedCustomerMessageProvider.scala
index 1fda89122..24308d239 100644
--- a/src/main/scala/code/customerinfo/MappedCustomerMessageProvider.scala
+++ b/src/main/scala/code/customer/MappedCustomerMessageProvider.scala
@@ -1,4 +1,4 @@
-package code.customerinfo
+package code.customer
import java.util.Date
diff --git a/src/main/scala/code/customer/MappedCustomerProvider.scala b/src/main/scala/code/customer/MappedCustomerProvider.scala
new file mode 100644
index 000000000..e7a24d60d
--- /dev/null
+++ b/src/main/scala/code/customer/MappedCustomerProvider.scala
@@ -0,0 +1,139 @@
+package code.customer
+
+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 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),
+ By(MappedCustomer.mBank, bankId.value))
+ }
+
+ override def getCustomerByCustomerId(customerId: String): Box[Customer] = {
+ MappedCustomer.find(
+ By(MappedCustomer.mCustomerId, customerId)
+ )
+ }
+
+ 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),
+ By(MappedCustomer.mBank, bankId.value)
+ )
+ }
+
+ override def getUser(bankId: BankId, customerNumber: String): Box[User] = {
+ MappedCustomer.find(
+ By(MappedCustomer.mBank, bankId.value),
+ By(MappedCustomer.mNumber, customerNumber)
+ ).flatMap(_.mUser.obj)
+ }
+
+ 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] = {
+
+ 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)
+ .mDependents(dependents)
+ .mHighestEducationAttained(highestEducationAttained)
+ .mEmploymentStatus(employmentStatus)
+ .mKycStatus(kycStatus)
+ .mLastOkDate(lastOkDate)
+ .saveMe()
+
+ Some(createdCustomer)
+ }
+
+}
+
+class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with IdPK with CreatedUpdated {
+
+ def getSingleton = MappedCustomer
+
+ object mCustomerId extends MappedUUID(this)
+
+ object mUser extends MappedLongForeignKey(this, APIUser)
+ object mBank extends DefaultStringField(this)
+
+ object mNumber extends DefaultStringField(this)
+ object mMobileNumber extends DefaultStringField(this)
+ object mLegalName extends DefaultStringField(this)
+ 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 customerId: String = mCustomerId.get // id.toString
+ override def number: String = mNumber.get
+ override def mobileNumber: String = mMobileNumber.get
+ override def legalName: String = mLegalName.get
+ override def email: String = mEmail.get
+ override def faceImage: CustomerFaceImage = new CustomerFaceImage {
+ 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.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.get
+ override def lastOkDate: Date = mLastOkDate.get
+}
+
+object MappedCustomer extends MappedCustomer with LongKeyedMetaMapper[MappedCustomer] {
+ //one customer info per bank for each api user
+ override def dbIndexes = UniqueIndex(mCustomerId) :: super.dbIndexes
+}
\ No newline at end of file
diff --git a/src/main/scala/code/customerinfo/CustomerInfoProvider.scala b/src/main/scala/code/customerinfo/CustomerInfoProvider.scala
deleted file mode 100644
index e7f16b310..000000000
--- a/src/main/scala/code/customerinfo/CustomerInfoProvider.scala
+++ /dev/null
@@ -1,34 +0,0 @@
-package code.customerinfo
-
-import java.util.Date
-
-import code.model.{BankId, User}
-import net.liftweb.common.Box
-import net.liftweb.util.SimpleInjector
-
-object CustomerInfo extends SimpleInjector {
-
- val customerInfoProvider = new Inject(buildOne _) {}
-
- def buildOne: CustomerInfoProvider = MappedCustomerInfoProvider
-
-}
-
-trait CustomerInfoProvider {
- def getInfo(bankId : BankId, user : User) : Box[CustomerInfo]
-
- def getUser(bankId : BankId, customerId : String) : Box[User]
-}
-
-trait CustomerInfo {
- def number : String
- def legalName : String
- def mobileNumber : String
- def email : String
- def faceImage : CustomerFaceImage
-}
-
-trait CustomerFaceImage {
- def url : String
- def date : Date
-}
\ No newline at end of file
diff --git a/src/main/scala/code/customerinfo/MappedCustomerInfoProvider.scala b/src/main/scala/code/customerinfo/MappedCustomerInfoProvider.scala
deleted file mode 100644
index 421207366..000000000
--- a/src/main/scala/code/customerinfo/MappedCustomerInfoProvider.scala
+++ /dev/null
@@ -1,53 +0,0 @@
-package code.customerinfo
-
-import java.util.Date
-
-import code.model.{BankId, User}
-import code.model.dataAccess.APIUser
-import code.util.DefaultStringField
-import net.liftweb.common.Box
-import net.liftweb.mapper._
-
-object MappedCustomerInfoProvider extends CustomerInfoProvider {
- override def getInfo(bankId : BankId, user: User): Box[CustomerInfo] = {
- MappedCustomerInfo.find(
- By(MappedCustomerInfo.mUser, user.apiId.value),
- By(MappedCustomerInfo.mBank, bankId.value))
- }
-
- override def getUser(bankId: BankId, customerNumber: String): Box[User] = {
- MappedCustomerInfo.find(
- By(MappedCustomerInfo.mBank, bankId.value),
- By(MappedCustomerInfo.mNumber, customerNumber)
- ).flatMap(_.mUser.obj)
- }
-}
-
-class MappedCustomerInfo extends CustomerInfo with LongKeyedMapper[MappedCustomerInfo] with IdPK with CreatedUpdated {
-
- def getSingleton = MappedCustomerInfo
-
- object mUser extends MappedLongForeignKey(this, APIUser)
- object mBank extends DefaultStringField(this)
-
- object mNumber extends DefaultStringField(this)
- object mMobileNumber extends DefaultStringField(this)
- object mLegalName extends DefaultStringField(this)
- object mEmail extends MappedEmail(this, 200)
- object mFaceImageUrl extends DefaultStringField(this)
- object mFaceImageTime extends MappedDateTime(this)
-
- override def number: String = mNumber.get
- override def mobileNumber: String = mMobileNumber.get
- override def legalName: String = mLegalName.get
- override def email: String = mEmail.get
- override def faceImage: CustomerFaceImage = new CustomerFaceImage {
- override def date: Date = mFaceImageTime.get
- override def url: String = mFaceImageUrl.get
- }
-}
-
-object MappedCustomerInfo extends MappedCustomerInfo with LongKeyedMetaMapper[MappedCustomerInfo] {
- //one customer info per bank for each api user
- override def dbIndexes = UniqueIndex(mBank, mNumber) :: UniqueIndex(mUser, mBank) :: super.dbIndexes
-}
\ No newline at end of file
diff --git a/src/main/scala/code/entitlement/Entilement.scala b/src/main/scala/code/entitlement/Entilement.scala
new file mode 100644
index 000000000..c1bad4ff7
--- /dev/null
+++ b/src/main/scala/code/entitlement/Entilement.scala
@@ -0,0 +1,27 @@
+package code.entitlement
+
+
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+
+object Entitlement extends SimpleInjector {
+
+ val entitlement = new Inject(buildOne _) {}
+
+ def buildOne: Entitlement = MappedEntitlement
+}
+
+trait Entitlement {
+ def entitlementId: String
+ def bankId : String
+ def userId : String
+ def roleName : String
+
+ def getEntitlement(bankId: String, userId: String, roleName: String) : Box[Entitlement]
+ def getEntitlement(entitlementId: String) : Box[Entitlement]
+ def getEntitlements(userId: String) : Box[List[Entitlement]]
+ def deleteEntitlement(entitlement: Box[Entitlement]) : Box[Boolean]
+ def getEntitlements() : Box[List[Entitlement]]
+ def addEntitlement(bankId: String, userId: String, roleName: String) : Box[Entitlement]
+}
\ No newline at end of file
diff --git a/src/main/scala/code/entitlement/MappedEntitlements.scala b/src/main/scala/code/entitlement/MappedEntitlements.scala
new file mode 100644
index 000000000..b6b3a8476
--- /dev/null
+++ b/src/main/scala/code/entitlement/MappedEntitlements.scala
@@ -0,0 +1,87 @@
+package code.entitlement
+
+
+import code.model.BankId
+import code.util.{DefaultStringField, MappedUUID}
+import net.liftweb.mapper._
+import net.liftweb.common.Box
+
+//object MappedEntitlementsProvider extends Entitlement {
+
+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
+
+
+ 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 getEntitlement(entitlementId: String): Box[Entitlement] = {
+ // Return a Box so we can handle errors later.
+ MappedEntitlement.find(
+ By(MappedEntitlement.mEntitlementId, entitlementId)
+ )
+ }
+
+ override def getEntitlements(userId: String): Box[List[Entitlement]] = {
+ // Return a Box so we can handle errors later.
+ Some(MappedEntitlement.findAll(
+ By(MappedEntitlement.mUserId, userId),
+ OrderBy(MappedEntitlement.updatedAt, Descending)))
+ }
+
+ override def getEntitlements: Box[List[MappedEntitlement]] = {
+ // Return a Box so we can handle errors later.
+ Some(MappedEntitlement.findAll(OrderBy(MappedEntitlement.updatedAt, Descending)))
+ }
+
+ override def deleteEntitlement(entitlement: Box[Entitlement]): Box[Boolean] = {
+ // Return a Box so we can handle errors later.
+ for {
+ findEntitlement <- entitlement
+ bankId <- Some(findEntitlement.bankId)
+ userId <- Some(findEntitlement.userId)
+ roleName <- Some(findEntitlement.roleName)
+ foundEntitlement <- MappedEntitlement.find(
+ By(MappedEntitlement.mBankId, bankId),
+ By(MappedEntitlement.mUserId, userId),
+ By(MappedEntitlement.mRoleName, roleName)
+ )
+ }
+ yield {
+ MappedEntitlement.delete_!(foundEntitlement)
+ }
+ }
+
+ override 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)
+ }
+}
+
+
+object MappedEntitlement extends MappedEntitlement with LongKeyedMetaMapper[MappedEntitlement] {
+ override def dbIndexes = UniqueIndex(mEntitlementId) :: super.dbIndexes
+}
\ No newline at end of file
diff --git a/src/main/scala/code/examplething/MappedThingProvider.scala b/src/main/scala/code/examplething/MappedThingProvider.scala
new file mode 100644
index 000000000..8fdf0991d
--- /dev/null
+++ b/src/main/scala/code/examplething/MappedThingProvider.scala
@@ -0,0 +1,54 @@
+package code.examplething
+
+
+import code.model.BankId
+
+
+import code.util.DefaultStringField
+import net.liftweb.common.Box
+import net.liftweb.mapper._
+
+
+
+object MappedThingProvider extends ThingProvider {
+
+ 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.bankId_, bankId.value)))
+ }
+}
+
+class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK {
+
+ override def getSingleton = MappedThing
+
+ object bankId_ extends DefaultStringField(this)
+ object name_ extends DefaultStringField(this)
+
+ object thingId_ extends DefaultStringField(this)
+
+ object fooSomething_ extends DefaultStringField(this)
+ object barSomething_ extends DefaultStringField(this)
+
+ override def thingId: ThingId = ThingId(thingId_.get)
+ override def something: String = name_.get
+
+
+ override def foo: Foo = new Foo {
+ override def fooSomething: String = fooSomething_
+ }
+
+ override def bar: Bar = new Bar {
+ override def barSomething: String = barSomething_
+ }
+
+
+}
+
+
+object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] {
+ 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
new file mode 100644
index 000000000..e53999ebb
--- /dev/null
+++ b/src/main/scala/code/examplething/Thing.scala
@@ -0,0 +1,84 @@
+package code.examplething
+
+
+// Need to import these one by one because in same package!
+import code.bankconnectors.{KafkaMappedConnector, LocalConnector, LocalMappedConnector}
+
+
+import code.model.{BankId}
+import net.liftweb.common.Logger
+import net.liftweb.util.{Props, SimpleInjector}
+
+object Thing extends SimpleInjector {
+
+ val thingProvider = new Inject(buildOne _) {}
+ // 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)
+
+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 or changing Things
+ Datasource implementation details are in Thing provider
+ */
+ final def getThings(bankId : BankId) : Option[List[Thing]] = {
+ getThingsFromProvider(bankId) match {
+ case Some(things) => {
+
+ val certainThings = for {
+ thing <- things // if thing.meta.license.name.size > 3
+ } yield thing
+ Option(certainThings)
+ }
+ case None => None
+ }
+ }
+
+ /*
+ Return one Thing
+ */
+ final def getThing(thingId : ThingId) : Option[Thing] = {
+ // Could do something here
+ getThingFromProvider(thingId) //.filter...
+ }
+
+ protected def getThingFromProvider(thingId : ThingId) : Option[Thing]
+ protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]]
+
+}
+
diff --git a/src/main/scala/code/fx/fx.scala b/src/main/scala/code/fx/fx.scala
new file mode 100644
index 000000000..1659c9eae
--- /dev/null
+++ b/src/main/scala/code/fx/fx.scala
@@ -0,0 +1,52 @@
+package code.fx
+
+import net.liftweb.common.Loggable
+
+/**
+ * Created by simonredfern on 14/04/2016.
+ */
+object fx extends Loggable {
+
+ val exchangeRates = {
+ Map(
+ "GBP" -> Map("EUR" -> 1.26, "USD" -> 1.42, "JPY" -> 154.47, "AED" -> 5.22, "INR" -> 94.66),
+ "EUR" -> Map("USD" -> 1.13, "JPY" -> 122.71, "AED" -> 4.14, "INR" -> 75.20, "GBP" -> 0.79),
+ "USD" -> Map("JPY" -> 108.77, "AED" -> 3.67, "INR" -> 66.65, "GBP" -> 0.70, "EUR" -> 0.89),
+ "JPY" -> Map("AED" -> 0.034, "INR" -> 0.61, "GBP" -> 0.0065, "EUR" -> 0.0081, "USD" -> 0.0092),
+ "AED" -> Map("INR" -> 18.15, "GBP" -> 0.19, "EUR" -> 0.24, "USD" -> 0.27, "JPY" -> 29.61),
+ "INR" -> Map("GBP" -> 0.011, "EUR" -> 0.013, "USD" -> 0.015, "JPY" -> 1.63, "AED" -> 0.055)
+ )
+ }
+
+
+
+ def convert(amount: BigDecimal, exchangeRate: Option[Double]): BigDecimal = {
+ val result = amount * exchangeRate.get
+ result.setScale(2, BigDecimal.RoundingMode.HALF_UP)
+ }
+
+
+ def exchangeRate(fromCurrency: String, toCurrency: String): Option[Double] = {
+
+ if (fromCurrency == toCurrency) {
+ Some(1)
+ } else {
+
+ //logger.debug(s"fromAmount is $fromAmount, toCurrency is ${toCurrency}")
+ val rate: Option[Double] = try {
+ // Get the translated name out of the map
+ Some(exchangeRates.get(fromCurrency).get(toCurrency))
+
+
+ }
+ catch {
+ case e: NoSuchElementException => None
+ }
+ rate
+ }
+ }
+
+}
+
+
+
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..4ccf906ce
--- /dev/null
+++ b/src/main/scala/code/kycdocuments/MappedKycDocumentsProvider.scala
@@ -0,0 +1,61 @@
+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 {
+
+ // TODO Add bankId (customerNumber is not unique)
+ 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/management/AccountsAPI.scala b/src/main/scala/code/management/AccountsAPI.scala
new file mode 100644
index 000000000..ace9e9d13
--- /dev/null
+++ b/src/main/scala/code/management/AccountsAPI.scala
@@ -0,0 +1,34 @@
+package code.management
+
+import code.api.{OBPRestHelper, APIFailure}
+import code.api.util.APIUtil._
+import code.model._
+import net.liftweb.common.{Box, Full, Failure, Loggable}
+import net.liftweb.http.js.JE.JsRaw
+import net.liftweb.http.rest.RestHelper
+
+object AccountsAPI extends OBPRestHelper with Loggable {
+ //needs to be a RestHelper to get access to JsonGet, JsonPost, etc.
+ self: RestHelper =>
+
+ val MODULE = "internal"
+ val VERSION = "v1.0"
+ val prefix = (MODULE / VERSION ).oPrefix(_)
+
+ oauthServe(prefix {
+ //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) ?~ "Account not found"
+ } yield {
+ 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/main/scala/code/management/ImporterAPI.scala b/src/main/scala/code/management/ImporterAPI.scala
new file mode 100644
index 000000000..5d6cf90c2
--- /dev/null
+++ b/src/main/scala/code/management/ImporterAPI.scala
@@ -0,0 +1,195 @@
+package code.management
+
+import java.util.Date
+
+import code.bankconnectors.Connector
+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
+import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString}
+import net.liftweb.json.{DefaultFormats, Extraction}
+import net.liftweb.util.Helpers._
+import net.liftweb.util.Props
+
+/**
+ * This is legacy code and does not handle edge cases very well and assumes certain things, e.g.
+ * that bank national identifier is unique (when in reality it should only be unique for a given
+ * country). So if it looks like it's doing things in a very weird way, that's because it is.
+ */
+object ImporterAPI extends RestHelper with Loggable {
+
+ case class TransactionsToInsert(l : List[ImporterTransaction])
+ case class InsertedTransactions(l : List[Transaction])
+
+ //models the json format -> This model cannot change or it will break the api call!
+ //if you want to update/modernize the json format, you will need to create a new api call
+ //and leave these models untouched until the old api call is no longer used by any clients
+ case class ImporterTransaction(obp_transaction : ImporterOBPTransaction)
+ case class ImporterOBPTransaction(this_account : ImporterAccount, other_account : ImporterAccount, details : ImporterDetails)
+ case class ImporterAccount(holder : String, number : String, kind : String, bank : ImporterBank)
+
+ /**
+ * it doesn't make sense that the IBAN attribute is on the Bank (as IBAN is in reality a property of an account),
+ * but this was how it was originally written and is also the
+ */
+ case class ImporterBank(IBAN : String, national_identifier : String, name : String)
+
+ case class ImporterDetails(kind : String, posted : ImporterDate,
+ completed : ImporterDate, new_balance : ImporterAmount, value : ImporterAmount,
+ label : String)
+ 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)
+
+ /**
+ * Legacy format
+ *
+ * TODO: can we just get rid of this? is anything using it?
+ */
+ def whenAddedJson(t : Transaction) : JObject = {
+
+ def formatDate(date : Date) : String = {
+ DefaultFormats.lossless.dateFormat.format(date)
+ }
+
+ val thisBank = Connector.connector.vend.getBank(t.bankId)
+ val thisAcc = Connector.connector.vend.getBankAccount(t.bankId, t.accountId)
+ val thisAccJson = JObject(List(JField("holder",
+ JObject(List(
+ JField("holder", JString(thisAcc.map(_.accountHolder).getOrElse(""))),
+ JField("alias", JString("no"))))),
+ JField("number", JString(thisAcc.map(_.number).getOrElse(""))),
+ JField("kind", JString(thisAcc.map(_.accountType).getOrElse(""))),
+ JField("bank", JObject(List( JField("IBAN", JString(thisAcc.flatMap(_.iban).getOrElse(""))),
+ JField("national_identifier", JString(thisBank.map(_.nationalIdentifier).getOrElse(""))),
+ JField("name", JString(thisBank.map(_.fullName).getOrElse(""))))))))
+
+ val otherAcc = t.otherAccount
+ val otherAccJson = JObject(List(JField("holder",
+ JObject(List(
+ JField("holder", JString(otherAcc.label)),
+ JField("alias", JString("no"))))),
+ JField("number", JString(otherAcc.number)),
+ JField("kind", JString(otherAcc.kind)),
+ JField("bank", JObject(List( JField("IBAN", JString(otherAcc.iban.getOrElse(""))),
+ JField("national_identifier", JString(otherAcc.nationalIdentifier)),
+ JField("name", JString(otherAcc.bankName)))))))
+
+ val detailsJson = JObject(List( JField("type_en", JString(t.transactionType)),
+ JField("type", JString(t.transactionType)),
+ JField("posted", JString(formatDate(t.startDate))),
+ JField("completed", JString(formatDate(t.finishDate))),
+ JField("other_data", JString("")),
+ JField("new_balance", JObject(List( JField("currency", JString(t.currency)),
+ JField("amount", JString(t.balance.toString))))),
+ JField("value", JObject(List( JField("currency", JString(t.currency)),
+ JField("amount", JString(t.amount.toString)))))))
+
+ val transactionJson = {
+ JObject(List(JField("obp_transaction_uuid", JString(t.uuid)),
+ JField("this_account", thisAccJson),
+ JField("other_account", otherAccJson),
+ JField("details", detailsJson)))
+ }
+
+ JObject(
+ List(
+ JField("obp_transaction", transactionJson)
+ )
+ )
+ }
+
+ serve {
+
+ case "obp_transactions_saver" :: "api" :: "transactions" :: Nil JsonPost json => {
+
+ def savetransactions = {
+ def updateBankAccountBalance(insertedTransactions : List[Transaction]) = {
+ if(insertedTransactions.nonEmpty) {
+ //we assume here that all the Envelopes concern only one account
+ val mostRecentTransaction = insertedTransactions.maxBy(t => t.finishDate)
+
+ Connector.connector.vend.updateAccountBalance(
+ mostRecentTransaction.bankId,
+ mostRecentTransaction.accountId,
+ mostRecentTransaction.balance)
+ }
+ }
+
+ val ipAddress = json._2.remoteAddr
+
+ val rawTransactions = json._1.children
+
+ logger.info("Received " + rawTransactions.size +
+ " json transactions to insert from ip address " + ipAddress)
+
+ //importer api expects dates that also include milliseconds (lossless)
+ val losslessFormats = net.liftweb.json.DefaultFormats.lossless
+ val mf = implicitly[Manifest[ImporterTransaction]]
+ val importerTransactions = rawTransactions.flatMap(j => j.extractOpt[ImporterTransaction](losslessFormats, mf))
+
+ logger.info("Received " + importerTransactions.size +
+ " valid json transactions to insert from ip address " + ipAddress)
+
+ if(importerTransactions.isEmpty) logger.warn("no transactions found to insert")
+
+ val toInsert = TransactionsToInsert(importerTransactions)
+
+ /**
+ * Using an actor to do insertions avoids concurrency issues with
+ * duplicate transactions by processing transaction batches one
+ * at a time. We'll have to monitor this to see if non-concurrent I/O
+ * is too inefficient. If it is, we could break it up into one actor
+ * per "Account".
+ */
+ // TODO: this duration limit should be fixed
+ val createdEnvelopes = TransactionInserter !? (3 minutes, toInsert)
+
+ createdEnvelopes match {
+ case Full(inserted : InsertedTransactions) =>
+ 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 _ => {
+ logger.warn("no envelopes inserted")
+ InternalServerErrorResponse()
+ }
+ }
+ }
+
+ S.param("secret") match {
+ case Full(s) => {
+ Props.get("importer_secret") match {
+ case Full(localS) =>
+ if(localS == s)
+ savetransactions
+ else
+ errorJsonResponse("wrong secret", 401)
+ case _ => errorJsonResponse("importer_secret not set on the server.")
+ }
+ }
+ case _ => errorJsonResponse("secret missing")
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/code/management/TransactionInserter.scala b/src/main/scala/code/management/TransactionInserter.scala
new file mode 100644
index 000000000..273fd3d45
--- /dev/null
+++ b/src/main/scala/code/management/TransactionInserter.scala
@@ -0,0 +1,118 @@
+package code.management
+
+import code.bankconnectors.Connector
+import code.model.Transaction
+import code.management.ImporterAPI._
+import net.liftweb.actor.LiftActor
+import net.liftweb.common._
+import net.liftweb.util.Helpers
+
+object TransactionInserter extends LiftActor with Loggable{
+
+ /**
+ * Determines whether two transactions to be imported are considered "identical"
+ *
+ * Currently this is considered true if the date cleared, the transaction amount,
+ * and the name of the other party are the same.
+ */
+ def isIdentical(t1: ImporterTransaction, t2: ImporterTransaction) : Boolean = {
+
+ val t1Details = t1.obp_transaction.details
+ val t1Completed = t1Details.completed
+ val t1Amount = t1Details.value.amount
+ val t1OtherAccHolder = t1.obp_transaction.other_account.holder
+
+ val t2Details = t2.obp_transaction.details
+ val t2Completed = t2Details.completed
+ val t2Amount = t2Details.value.amount
+ val t2OtherAccHolder = t2.obp_transaction.other_account.holder
+
+ t1Completed == t2Completed &&
+ t1Amount == t2Amount &&
+ t1OtherAccHolder == t2OtherAccHolder
+ }
+
+ /**
+ * Inserts a list of identical transactions, ensuring that no duplicates are made.
+ *
+ * This is done by querying for all existing copies of this transaction,
+ * and comparing the number of results to the size of the list of transactions to insert.
+ *
+ * E.g. If this method receives 3 identical transactions, and only 1 copy exists
+ * in the database, then 2 more should be added.
+ *
+ * If this method receives 3 identical transactions, and 3 copies exist in the
+ * database, then 0 more should be added.
+ */
+ def insertIdenticalTransactions(identicalTransactions : List[ImporterTransaction]) : List[Transaction] = {
+ if(identicalTransactions.size == 0){
+ Nil
+ }else{
+ //we don't want to be putting people's transaction info in the logs, so we use an id
+ val insertID = Helpers.randomString(10)
+ logger.info("Starting insert operation, id: " + insertID)
+
+ val toMatch = identicalTransactions(0)
+
+ val existingMatches = Connector.connector.vend.getMatchingTransactionCount(
+ toMatch.obp_transaction.this_account.bank.national_identifier,
+ toMatch.obp_transaction.this_account.number,
+ toMatch.obp_transaction.details.value.amount,
+ toMatch.obp_transaction.details.completed.`$dt`,
+ toMatch.obp_transaction.other_account.holder)
+
+ //logger.info("Insert operation id " + insertID + " # of existing matches: " + existingMatches)
+ val numberToInsert = identicalTransactions.size - existingMatches
+ 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
+
+ results.foreach{
+ case Failure(msg, _, _) => logger.warn(s"create transaction failed: $msg")
+ case Empty => logger.warn("create transaction failed")
+ case _ => Unit //do nothing
+ }
+
+ results.flatten
+ }
+ }
+
+ def messageHandler = {
+ case TransactionsToInsert(ts : List[ImporterTransaction]) => {
+
+ /**
+ * Example:
+ * input : List(A,B,C,C,D,E,E,E,F)
+ * output: List(List(A), List(B), List(C,C), List(D), List(E,E,E), List(F))
+ *
+ * This lets us run an insert function on each list of identical transactions that will
+ * avoid inserting duplicates.
+ */
+
+ def groupIdenticals(list : List[ImporterTransaction]) : List[List[ImporterTransaction]] = {
+ list match{
+ case Nil => Nil
+ case h::Nil => List(list)
+ case h::t => {
+ //transactions that are identical to the head of the list
+ val matches = list.filter(isIdentical(h, _))
+ List(matches) ++ groupIdenticals(list diff matches)
+ }
+ }
+ }
+
+ val grouped = groupIdenticals(ts)
+
+ val insertedTransactions =
+ grouped
+ .map(identicals => insertIdenticalTransactions(identicals))
+ .flatten
+
+ reply(
+ InsertedTransactions(insertedTransactions)
+ )
+ }
+ }
+
+}
diff --git a/src/main/scala/code/meetings/MappedMeetingProvider.scala b/src/main/scala/code/meetings/MappedMeetingProvider.scala
new file mode 100644
index 000000000..14414a419
--- /dev/null
+++ b/src/main/scala/code/meetings/MappedMeetingProvider.scala
@@ -0,0 +1,101 @@
+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 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(
+ // TODO Need to check permissions (user)
+ MappedMeeting.mBankId, bankId.toString),
+ OrderBy(MappedMeeting.mWhen, Descending)))
+ }
+
+
+
+ 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)
+ //.mStaffUserId(staffUser.apiId.value)
+ .mCustomerUserId(customerUser.apiId.value)
+ .mProviderId(providerId)
+ .mPurposeId(purposeId)
+ .mWhen(when)
+ .mSessionId(sessionId)
+ .mCustomerToken(customerToken)
+ .mStaffToken(staffToken)
+ .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.get, mCustomerToken.get, mStaffToken.get)
+ override def present = MeetingPresent(staffUserId = mStaffUserId.get.toString,
+ customerUserId = mCustomerUserId.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..da5f32385
--- /dev/null
+++ b/src/main/scala/code/meetings/Meeting.scala
@@ -0,0 +1,49 @@
+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, 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]
+}
+
+
+
+
diff --git a/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala b/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala
index 06a5b4fc6..b9ccb9795 100644
--- a/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala
+++ b/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala
@@ -1,15 +1,15 @@
package code.metadata.counterparties
-import java.util.Date
+import java.util.{UUID, Date}
import code.model._
import code.model.dataAccess.APIUser
import code.util.{MappedAccountNumber, DefaultStringField, MappedUUID}
-import net.liftweb.common.{Box, Full}
+import net.liftweb.common.{Loggable, Box, Full}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo
-object MapperCounterparties extends Counterparties {
+object MapperCounterparties extends Counterparties with Loggable {
override def getOrCreateMetadata(originalPartyBankId: BankId, originalPartyAccountId: AccountId, otherParty: OtherBankAccount): OtherBankAccountMetadata = {
/**
@@ -17,8 +17,7 @@ object MapperCounterparties extends Counterparties {
* for the account in question
*/
def newPublicAliasName(): String = {
- import scala.util.Random
- val firstAliasAttempt = "ALIAS_" + Random.nextLong().toString.take(6)
+ val firstAliasAttempt = "ALIAS_" + UUID.randomUUID.toString.toUpperCase.take(6)
/**
* Returns true if @publicAlias is already the name of a public alias within @account
@@ -35,7 +34,8 @@ object MapperCounterparties extends Counterparties {
* Appends things to @publicAlias until it a unique public alias name within @account
*/
def appendUntilUnique(publicAlias: String): String = {
- val newAlias = publicAlias + Random.nextLong().toString.take(1)
+ val newAlias = publicAlias + UUID.randomUUID.toString.toUpperCase.take(1)
+ // Recursive call.
if (isDuplicate(newAlias)) appendUntilUnique(newAlias)
else newAlias
}
@@ -46,7 +46,7 @@ object MapperCounterparties extends Counterparties {
//can't find by MappedCounterpartyMetadata.counterpartyId = otherParty.id because in this implementation
- //if the metadata doesn't exist, the id field of ther OtherBankAccount is not known yet, and will be empty
+ //if the metadata doesn't exist, the id field of the OtherBankAccount is not known yet, and will be empty
def findMappedCounterpartyMetadata(originalPartyBankId: BankId, originalPartyAccountId: AccountId,
otherParty: OtherBankAccount) : Box[MappedCounterpartyMetadata] = {
MappedCounterpartyMetadata.find(
@@ -60,16 +60,26 @@ object MapperCounterparties extends Counterparties {
existing match {
case Full(e) => e
- case _ => MappedCounterpartyMetadata.create
- .thisAccountBankId(originalPartyBankId.value)
- .thisAccountId(originalPartyAccountId.value)
- .holder(otherParty.label)
- .publicAlias(newPublicAliasName())
- .accountNumber(otherParty.number).saveMe
+ // Create it!
+ case _ => {
+ logger.debug("Before creating MappedCounterpartyMetadata")
+ // Store a record that contains counterparty information from the perspective of an account at a bank
+ MappedCounterpartyMetadata.create
+ // Core info
+ .thisAccountBankId(originalPartyBankId.value)
+ .thisAccountId(originalPartyAccountId.value)
+ .holder(otherParty.label) // The main human readable identifier for this counter party from the perspective of the account holder
+ .publicAlias(newPublicAliasName()) // The public alias this account gives to the counterparty.
+ .accountNumber(otherParty.number)
+ // otherParty.metadata is None at this point
+ //.imageUrl("www.example.com/image.jpg")
+ //.moreInfo("This is hardcoded moreInfo")
+ .saveMe
+ }
}
}
- //get all counterparty metadatas for a single OBP account
+ // Get all counterparty metadata for a single OBP account
override def getMetadatas(originalPartyBankId: BankId, originalPartyAccountId: AccountId): List[OtherBankAccountMetadata] = {
MappedCounterpartyMetadata.findAll(
By(MappedCounterpartyMetadata.thisAccountBankId, originalPartyBankId.value),
diff --git a/src/main/scala/code/metrics/APIMetrics.scala b/src/main/scala/code/metrics/APIMetrics.scala
index 5ed4e712d..606212d1c 100644
--- a/src/main/scala/code/metrics/APIMetrics.scala
+++ b/src/main/scala/code/metrics/APIMetrics.scala
@@ -1,13 +1,18 @@
package code.metrics
-import net.liftweb.util.SimpleInjector
+import net.liftweb.util.{Props, SimpleInjector}
import java.util.{Calendar, Date}
object APIMetrics extends SimpleInjector {
val apiMetrics = new Inject(buildOne _) {}
- def buildOne: APIMetrics = MappedMetrics
+ def buildOne: APIMetrics =
+ Props.getBool("allow_elasticsearch", false) &&
+ Props.getBool("allow_elasticsearch_metrics", false) match {
+ case false => MappedMetrics
+ case true => ElasticsearchMetrics
+ }
/**
* Returns a Date which is at the start of the day of the date
@@ -29,7 +34,12 @@ object APIMetrics extends SimpleInjector {
trait APIMetrics {
- def saveMetric(url : String, date : Date) : Unit
+ def saveMetric(userId: String, url : String, date : Date, userName: String, appName: String, developerEmail: String) : Unit
+
+ def saveMetric(url : String, date : Date) : Unit ={
+ //TODO: update all places calling old function before removing this
+ saveMetric ("TODO: userId", url, date, "TODO: userName", "TODO: appName", "TODO: developerEmail")
+ }
//TODO: ordering of list? should this be by date? currently not enforced
def getAllGroupedByUrl() : Map[String, List[APIMetric]]
@@ -37,10 +47,17 @@ 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
+ def getUserName() : String
+ def getAppName : String
+ def getDeveloperEmail() : String
}
diff --git a/src/main/scala/code/metrics/ElasticsearchMetrics.scala b/src/main/scala/code/metrics/ElasticsearchMetrics.scala
new file mode 100644
index 000000000..ed50d5e49
--- /dev/null
+++ b/src/main/scala/code/metrics/ElasticsearchMetrics.scala
@@ -0,0 +1,34 @@
+package code.metrics
+
+import java.util.Date
+
+import code.search.elasticsearchMetrics
+import net.liftweb.util.Props
+
+object ElasticsearchMetrics extends APIMetrics {
+
+ val es = new elasticsearchMetrics
+
+ override def saveMetric(userId: String, url: String, date: Date, userName: String, appName: String, developerEmail: String): Unit = {
+ if (Props.getBool("allow_elasticsearch", false) && Props.getBool("allow_elasticsearch_metrics", false) ) {
+
+ es.indexMetric(userId, url, date, userName, appName, developerEmail)
+ }
+ }
+
+ override def getAllGroupedByUserId(): Map[String, List[APIMetric]] = {
+ //TODO: replace the following with valid ES query
+ MappedMetric.findAll.groupBy(_.getUserId)
+ }
+
+ override def getAllGroupedByDay(): Map[Date, List[APIMetric]] = {
+ //TODO: replace the following with valid ES query
+ MappedMetric.findAll.groupBy(APIMetrics.getMetricDay)
+ }
+
+ override def getAllGroupedByUrl(): Map[String, List[APIMetric]] = {
+ //TODO: replace the following with valid ES query
+ MappedMetric.findAll.groupBy(_.getUrl())
+ }
+
+}
diff --git a/src/main/scala/code/metrics/MappedMetrics.scala b/src/main/scala/code/metrics/MappedMetrics.scala
index e4ea85537..3f3a6d34b 100644
--- a/src/main/scala/code/metrics/MappedMetrics.scala
+++ b/src/main/scala/code/metrics/MappedMetrics.scala
@@ -7,8 +7,13 @@ import net.liftweb.mapper._
object MappedMetrics extends APIMetrics {
- override def saveMetric(url: String, date: Date): Unit = {
- MappedMetric.create.url(url).date(date).save
+ override def saveMetric(userId: String, url: String, date: Date, userName: String, appName: String, developerEmail: String): Unit = {
+ MappedMetric.create.url(url).date(date).userName(userName).appName(appName).developerEmail(developerEmail).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]] = {
@@ -26,15 +31,22 @@ 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)
+ object userName extends DefaultStringField(this)
+ object appName extends DefaultStringField(this)
+ object developerEmail extends DefaultStringField(this)
override def getUrl(): String = url.get
override def getDate(): Date = date.get
-
+ override def getUserId(): String = userId.get
+ override def getUserName(): String = userName.get
+ override def getAppName(): String = appName.get
+ override def getDeveloperEmail(): String = developerEmail.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) :: Index(userName) :: Index(appName) :: Index(developerEmail) :: super.dbIndexes
}
diff --git a/src/main/scala/code/metrics/MongoAPIMetric.scala b/src/main/scala/code/metrics/MongoAPIMetric.scala
index eae5d93b0..1afde9e64 100644
--- a/src/main/scala/code/metrics/MongoAPIMetric.scala
+++ b/src/main/scala/code/metrics/MongoAPIMetric.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
@@ -32,26 +32,39 @@ 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)
+ object userName extends StringField(this,255)
+ object appName extends StringField(this,255)
+ object developerEmail extends StringField(this,255)
- def getUrl() = url.get
- def getDate() = date.get
+ def getUrl() = url.get
+ def getDate() = date.get
+ def getUserId() = userId.get
+ def getUserName(): String = userName.get
+ def getAppName(): String = appName.get
+ def getDeveloperEmail(): String = developerEmail.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, userName: String, appName: String, developerEmail: String) : Unit = {
MongoAPIMetric.createRecord.
+ userId(userId).
url(url).
date(date).
+ userName(userName).
+ appName(appName).
+ developerEmail(developerEmail).
save
}
@@ -63,4 +76,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/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala
index f84de80c2..07cc1afe8 100644
--- a/src/main/scala/code/model/BankingData.scala
+++ b/src/main/scala/code/model/BankingData.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
@@ -72,6 +72,30 @@ object TransactionId {
def unapply(id : String) = Some(TransactionId(id))
}
+case class TransactionRequestType(val value : String) {
+ override def toString = value
+}
+
+object TransactionRequestType {
+ def unapply(id : String) = Some(TransactionRequestType(id))
+}
+
+case class TransactionRequestId(val value : String) {
+ override def toString = value
+}
+
+object TransactionRequestId {
+ def unapply(id : String) = Some(TransactionRequestId(id))
+}
+
+case class TransactionTypeId(val value : String) {
+ override def toString = value
+}
+
+object TransactionTypeId {
+ def unapply(id : String) = Some(TransactionTypeId(id))
+}
+
case class AccountId(val value : String) {
override def toString = value
}
@@ -88,6 +112,25 @@ 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
+}
+
+object CounterpartyId {
+ def unapply(id : String) = Some(CounterpartyId(id))
+}
+
trait Bank {
def bankId: BankId
def shortName : String
@@ -95,7 +138,13 @@ trait Bank {
def logoUrl : String
def websiteUrl : String
- //it's not entirely clear what this is/represents
+ // TODO Add Group ?
+
+
+ //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] = {
@@ -164,23 +213,37 @@ class AccountOwner(
case class BankAccountUID(bankId : BankId, accountId : AccountId)
+
+
+/** Internal model of a Bank Account
+ * @define accountType The account type aka financial product name. The customer friendly text that identifies the financial product this account is based on, as given by the bank
+ * @define accountId An identifier (no spaces, url friendly, should be a UUID) that hides the actual account number (obp identifier)
+ * @define number The actual bank account number as given by the bank to the customer
+ * @define bankId The short bank identifier that holds this account (url friendly, usually short name of bank with hyphens)
+ * @define label A string that helps identify the account to a customer or the public. Can be updated by the account owner. Default would typically include the owner display name (should be legal entity owner) + accountType + few characters of number
+ * @define iban The IBAN (could be empty)
+ * @define currency The currency (3 letter code)
+ * @define balance The current balance on the account
+ */
+
+// TODO Add: @define productCode A code (no spaces, url friendly) that identifies the financial product this account is based on.
+
trait BankAccount {
@transient protected val log = Logger(this.getClass)
- @deprecated
- def uuid : String
-
def accountId : AccountId
- def accountType : String
+ def accountType : String // (stored in the field "kind" on Mapper)
+ //def productCode : String // TODO Add this shorter code.
def balance : BigDecimal
def currency : String
- def name : String
+ def name : String // Is this used?
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 lastUpdate : Date
@deprecated("Get the account holder(s) via owners")
def accountHolder : String
@@ -192,6 +255,26 @@ 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): Box[Boolean] = {
+ if(user.ownerAccess(this)){
+ Full(Connector.connector.vend.removeAccount(this.bankId, this.accountId))
+ } else {
+ // TODO Correct English in failure messages (but consider compatibility of messages with older API versions)
+ Failure("user : " + user.emailAddress + "don't have access to owner view on account " + accountId, Empty, Empty)
+ }
+ }
+
+ final def updateLabel(user : User, label : String): Box[Boolean] = {
+ if(user.ownerAccess(this)){
+ Full(Connector.connector.vend.updateAccountLabel(this.bankId, this.accountId, label))
+ } else {
+ Failure("user : " + user.emailAddress + "don't have access to owner view on account " + accountId, Empty, Empty)
+ }
+ }
+
final def owners: Set[User] = {
val accountHolders = Connector.connector.vend.getAccountHolders(bankId, accountId)
if(accountHolders.isEmpty) {
@@ -199,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 = ""
@@ -216,7 +300,7 @@ trait BankAccount {
user match {
case Full(u) => u.permittedViews(this)
case _ =>{
- log.info("no user was found in the permittedViews")
+ log.info("No user was passed to permittedViews")
publicViews
}
}
@@ -339,15 +423,20 @@ trait BankAccount {
Failure("user : " + user.emailAddress + " don't have access to owner view on account " + accountId, Empty, Empty)
}
+
+ /*
+ views
+ */
+
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
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 {
@@ -362,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 {
@@ -379,16 +468,16 @@ trait BankAccount {
final def removeView(userDoingTheRemove : User, viewId: ViewId) : Box[Unit] = {
if(!userDoingTheRemove.ownerAccess(this)) {
- Failure({"user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " does not have owner access"})
+ return Failure({"user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " does not have owner access"})
} else {
val deleted = Views.views.vend.removeView(viewId, this)
-
- if(deleted.isDefined) {
- log.info("user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " deleted view: " + viewId +
- " for account " + accountId + "at bank " + bankId)
+
+ if (deleted.isDefined) {
+ log.info("user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " deleted view: " + viewId +
+ " for account " + accountId + "at bank " + bankId)
}
-
- deleted
+
+ return deleted
}
}
@@ -401,6 +490,10 @@ trait BankAccount {
viewNotAllowed(view)
}
+ /*
+ end views
+ */
+
final def getModeratedTransactions(user : Box[User], view : View, queryParams: OBPQueryParam*): Box[List[ModeratedTransaction]] = {
if(authorizedAccess(view, user)) {
for {
@@ -472,23 +565,27 @@ object BankAccount {
}
}
+/*
+The other bank account or counterparty in a transaction
+as see from the perspective of the original party.
+ */
+
class OtherBankAccount(
val id : String,
- val label : String,
- val nationalIdentifier : String,
- //the bank international identifier
- val swift_bic : Option[String],
- //the international account identifier
- val iban : Option[String],
- val number : String,
- val bankName : String,
- val kind : String,
+ val label : String, // Reference given to the counterparty by the original party.
+ val nationalIdentifier : String, // National identifier for a bank account (how is this different to number below?)
+ val swift_bic : Option[String], // The international bank identifier
+ val iban : Option[String], // The international account identifier
+ val number : String, // Bank account number for the counterparty
+ val bankName : String, // Bank name of counterparty. What if the counterparty is not a bank? Rename to institution?
+ val kind : String, // Type of bank account.
val originalPartyBankId: BankId, //bank id of the party for which this OtherBankAccount is the counterparty
val originalPartyAccountId: AccountId, //account id of the party for which this OtherBankAccount is the counterparty
val alreadyFoundMetadata : Option[OtherBankAccountMetadata]
) {
val metadata : OtherBankAccountMetadata = {
+ // If we already have alreadyFoundMetadata, return it, else get or create it.
alreadyFoundMetadata match {
case Some(meta) =>
meta
@@ -556,4 +653,9 @@ class Transaction(
WhereTags.whereTags.vend.addWhereTag(bankId, accountId, id) _,
WhereTags.whereTags.vend.deleteWhereTag(bankId, accountId, id) _
)
-}
\ No newline at end of file
+}
+
+case class AmountOfMoney (
+ val currency: String,
+ val amount: String
+)
\ No newline at end of file
diff --git a/src/main/scala/code/model/Metadata.scala b/src/main/scala/code/model/Metadata.scala
index ac2726ef8..50f94986a 100644
--- a/src/main/scala/code/model/Metadata.scala
+++ b/src/main/scala/code/model/Metadata.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
@@ -100,6 +100,9 @@ trait TransactionImage {
def imageUrl : URL
}
+/*
+Counterparty metadata
+ */
trait OtherBankAccountMetadata {
def metadataId: String
def getHolder: String
diff --git a/src/main/scala/code/model/ModeratedBankingData.scala b/src/main/scala/code/model/ModeratedBankingData.scala
index 5b323104c..0cd599b2a 100644
--- a/src/main/scala/code/model/ModeratedBankingData.scala
+++ b/src/main/scala/code/model/ModeratedBankingData.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/main/scala/code/model/OAuth.scala b/src/main/scala/code/model/OAuth.scala
index 1552d99f2..8c287b163 100644
--- a/src/main/scala/code/model/OAuth.scala
+++ b/src/main/scala/code/model/OAuth.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
@@ -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,35 +123,30 @@ 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 =
-
-
- Total of {Consumer.count} applications from {recordsWithUniqueEmails.getOrElse("ERROR")} unique email addresses.
- {recordsWithUniqueAppNames.getOrElse("ERROR")} unique app names.
-
-
-
-
-
+
+ Total of {Consumer.count} applications from {recordsWithUniqueEmails.getOrElse("ERROR")} unique email addresses.
+ {recordsWithUniqueAppNames.getOrElse("ERROR")} unique app names.
+
@@ -221,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()
@@ -242,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/User.scala b/src/main/scala/code/model/User.scala
index 7b9afee4d..b1dcdb4a9 100644
--- a/src/main/scala/code/model/User.scala
+++ b/src/main/scala/code/model/User.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
@@ -45,9 +45,13 @@ 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
+ def userId: String
def idGivenByProvider: String
def provider : String
@@ -63,7 +67,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")
}
}
@@ -96,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/model/View.scala b/src/main/scala/code/model/View.scala
index afc56760b..5bd384595 100644
--- a/src/main/scala/code/model/View.scala
+++ b/src/main/scala/code/model/View.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
@@ -52,7 +52,12 @@ case class Permission(
views : List[View]
)
-trait ViewData {
+
+/*
+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
def is_public: Boolean
def which_alias_to_use: String
@@ -60,23 +65,122 @@ trait ViewData {
def allowed_actions : List[String]
}
-case class ViewCreationJSON(
+/*
+The JSON that should be supplied to create a view. Conforms to ViewSpecification
+ */
+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(
+/*
+The JSON that should be supplied to update a view. Conforms to ViewSpecification
+ */
+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
+
+
+
+/** Views moderate access to an Account. That is, they are used to:
+ * 1) Show/hide fields on the account, its transactions and related counterparties
+ * 2) Store/partition meta data - e.g. comments posted on a "team" view are not visible via a "public" view and visa versa.
+ *
+ * Users can be granted access to one or more Views
+ * Each View has a set of entitlements aka permissions which hide / show data fields and enable / disable operations on the account
+ *
+ * @define viewId A short url friendly, (singular) human readable name for the view. e.g. "team", "auditor" or "public". Note: "owner" is a default and reserved name. Other reserved names should include "public", "accountant" and "auditor"
+ * @define accountId The account that the view moderates
+ * @define bankId The bank where the account is held
+ * @define name The name of the view
+ * @define description A description of the view
+ * @define isPublic Set to True if the view should be open to the public (no authorisation required!) Set to False to require authorisation
+ * @define users A list of users that can use this view
+ * @define usePublicAliasIfOneExists If true and the counterparty in a transaciton has a public alias set, use it. Else use the raw counterparty name (if both usePublicAliasIfOneExists and usePrivateAliasIfOneExists are true, public alias will be used)
+ * @define usePrivateAliasIfOneExists If true and the counterparty in a transaciton has a private alias set, use it. Else use the raw counterparty name (if both usePublicAliasIfOneExists and usePrivateAliasIfOneExists are true, public alias will be used)
+ * @define hideOtherAccountMetadataIfAlias If true, the view will hide counterparty metadata if the counterparty has an alias. This is to preserve anonymity if required.
+ *
+ * @define canSeeTransactionThisBankAccount If true, the view will show information about the Transaction account (this account)
+ * @define canSeeTransactionOtherBankAccount If true, the view will show information about the Transaciton counterparty
+ * @define canSeeTransactionMetadata If true, the view will show any Transaction metadata
+ * @define canSeeTransactionDescription If true, the view will show the Transaction description
+ * @define canSeeTransactionAmount If true, the view will show the Transaction amount (value, not currency)
+ * @define canSeeTransactionType If true, the view will show the Transaction type
+ * @define canSeeTransactionCurrency If true, the view will show the Transaction currency (not value)
+ * @define canSeeTransactionStartDate If true, the view will show the Transaction start date
+ * @define canSeeTransactionFinishDate If true, the view will show the Transaction finish date
+ * @define canSeeTransactionBalance If true, the view will show the Transaction balance (after each transaction)
+ *
+ * @define canSeeComments If true, the view will show the Transaction Metadata comments
+ * @define canSeeOwnerComment If true, the view will show the Transaction Metadata owner comment
+ * @define canSeeTags If true, the view will show the Transaction Metadata tags
+ * @define canSeeImages If true, the view will show the Transaction Metadata images
+
+ * @define canSeeBankAccountOwners If true, the view will show the Account owners
+ * @define canSeeBankAccountType If true, the view will show the Account type. The account type is a human friendly financial product name
+ * @define canSeeBankAccountBalance If true, the view will show the Account balance
+ * @define canSeeBankAccountCurrency If true, the view will show the Account currency
+ * @define canSeeBankAccountLabel If true, the view will show the Account label. The label can be edited via the API. It does not come from the core banking system.
+ * @define canSeeBankAccountNationalIdentifier If true, the view will show the national identifier of the bank
+ * @define canSeeBankAccountSwift_bic If true, the view will show the Swift / Bic code of the bank
+ * @define canSeeBankAccountIban If true, the view will show the IBAN
+ * @define canSeeBankAccountNumber If true, the view will show the account number
+ * @define canSeeBankAccountBankName If true, the view will show the bank name
+
+ * @define canSeeOtherAccountNationalIdentifier If true, the view will show the Counterparty bank national identifier
+ * @define canSeeOtherAccountSWIFT_BIC If true, the view will show the Counterparty SWIFT BIC
+ * @define canSeeOtherAccountIBAN If true, the view will show the Counterparty IBAN
+ * @define canSeeOtherAccountBankName If true, the view will show the Counterparty Bank Name
+ * @define canSeeOtherAccountNumber If true, the view will show the Counterparty Account Number
+ * @define canSeeOtherAccountMetadata If true, the view will show the Counterparty Metadata
+ * @define canSeeOtherAccountKind If true, the view will show the Counterparty Account Type. This is unlikely to be a full financial product name.
+
+ * @define canSeeMoreInfo If true, the view will show the Counterparty More Info text
+ * @define canSeeUrl If true, the view will show the Counterparty Url
+ * @define canSeeImageUrl If true, the view will show the Counterparty Image Url
+ * @define canSeeOpenCorporatesUrl If true, the view will show the Counterparty OpenCorporatesUrl
+ * @define canSeeCorporateLocation If true, the view will show the Counterparty CorporateLocation
+ * @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 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
+ * @define canAddCorporateLocation If true, the view can add the Counterparty CorporateLocation
+ * @define canAddPhysicalLocation If true, the view can add the Counterparty PhysicalLocation
+ * @define canAddPublicAlias If true, the view can add the Counterparty PublicAlias
+ * @define canAddPrivateAlias If true, the view can add the Counterparty PrivateAlias
+ * @define canDeleteCorporateLocation If true, the can add show the Counterparty CorporateLocation
+ * @define canDeletePhysicalLocation If true, the can add show the Counterparty PhysicalLocation
+ *
+ * @define canEditOwnerComment If true, the view can edit the Transaction Owner Comment
+ * @define canAddComment If true, the view can add a Transaciton Comment
+ * @define canDeleteComment If true, the view can delete a Transaciton Comment
+ * @define canAddTag If true, the view can add a Transaciton Tag
+ * @define canDeleteTag If true, the view can delete a Transaciton Tag
+ * @define canAddImage If true, the view can add a Transaciton Image
+ * @define canDeleteImage If true, the view can delete a Transaciton Image
+ * @define canAddWhereTag If true, the view can add a Transaciton Where Tag
+ * @define canSeeWhereTag If true, the view can show the Transaction Where Tag
+ * @define canDeleteWhereTag If true, the view can delete the Transaction Where Tag
+
+ * @define canInitiateTransaction If true, view can initiate Transaction Request. Note. Owner view may be required. TODO check this.
+
+
+ */
+
trait View {
+
val viewLogger = Logger(classOf[View])
//e.g. "Public", "Authorities", "Our Network", etc.
@@ -130,7 +234,7 @@ trait View {
def canSeeBankAccountNumber : Boolean
def canSeeBankAccountBankName : Boolean
- //other bank account fields
+ //other bank account (counterparty) fields
def canSeeOtherAccountNationalIdentifier : Boolean
def canSeeOtherAccountSWIFT_BIC : Boolean
def canSeeOtherAccountIBAN : Boolean
@@ -139,7 +243,7 @@ trait View {
def canSeeOtherAccountMetadata : Boolean
def canSeeOtherAccountKind : Boolean
- //other bank account meta data
+ //other bank account meta data - read
def canSeeMoreInfo: Boolean
def canSeeUrl: Boolean
def canSeeImageUrl: Boolean
@@ -148,6 +252,8 @@ trait View {
def canSeePhysicalLocation : Boolean
def canSeePublicAlias : Boolean
def canSeePrivateAlias : Boolean
+
+ //other bank account (Counterparty) meta data - write
def canAddMoreInfo : Boolean
def canAddURL : Boolean
def canAddImageURL : Boolean
@@ -171,7 +277,6 @@ trait View {
def canSeeWhereTag : Boolean
def canDeleteWhereTag : Boolean
- // transfer methods
def canInitiateTransaction: Boolean
def moderate(transaction : Transaction): Box[ModeratedTransaction] = {
diff --git a/src/main/scala/code/model/dataAccess/APIUser.scala b/src/main/scala/code/model/dataAccess/APIUser.scala
index 47560f040..16fe4b107 100644
--- a/src/main/scala/code/model/dataAccess/APIUser.scala
+++ b/src/main/scala/code/model/dataAccess/APIUser.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
@@ -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
}
@@ -60,6 +62,17 @@ class APIUser extends LongKeyedMapper[APIUser] with User with ManyToMany with On
object views_ extends MappedManyToMany(ViewPrivileges, ViewPrivileges.user, ViewPrivileges.view, ViewImpl)
+ // Roles
+ // THESE ARE NO LONGER USED!!------------------------
+ object hasCrmAdminRole extends MappedBoolean(this)
+ object hasCrmReaderRole extends MappedBoolean(this)
+ object hasCustomerMessageAdminRole extends MappedBoolean(this)
+
+ object hasBranchReaderRole extends MappedBoolean(this)
+ object hasAtmReaderRole extends MappedBoolean(this)
+ object hasProductReaderRole extends MappedBoolean(this)
+ // END of no longer used. TODO remove. --------------
+
def emailAddress = {
val e = email.get
if(e != null) e else ""
@@ -68,10 +81,21 @@ 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
+ // Depreciated. Do not use.///////////////
+ def isCrmAdmin : Boolean = hasCrmAdminRole
+ def isCrmReader : Boolean = hasCrmReaderRole
+ def isCustomerMessageAdmin : Boolean = hasCustomerMessageAdminRole
+
+ def isBranchReader : Boolean = hasBranchReaderRole
+ def isAtmReader : Boolean = hasAtmReaderRole
+ def isProductReader : Boolean = hasProductReaderRole
+ ////////////////////////////////////////////////////
}
object APIUser extends APIUser with LongKeyedMetaMapper[APIUser]{
diff --git a/src/main/scala/code/model/dataAccess/Account.scala b/src/main/scala/code/model/dataAccess/Account.scala
index 2429e8600..1d519c05e 100644
--- a/src/main/scala/code/model/dataAccess/Account.scala
+++ b/src/main/scala/code/model/dataAccess/Account.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
@@ -32,28 +32,17 @@ Berlin 13359, Germany
package code.model.dataAccess
-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 java.util.Date
+
+import code.bankconnectors.{OBPLimit, OBPOffset, OBPOrdering, _}
import code.model._
+import com.mongodb.QueryBuilder
+import net.liftweb.common._
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
-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{
@@ -69,6 +58,9 @@ class Account extends BankAccount with MongoRecord[Account] with ObjectIdPk[Acco
override def name = "number"
}
object kind extends StringField(this, 255)
+
+ // object productCode extends StringField(this, 255)
+
object accountName extends StringField(this, 255){
//this is the legacy db field name
override def name = "name"
@@ -87,7 +79,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
@@ -157,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)
@@ -178,6 +168,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] {
@@ -193,21 +184,22 @@ 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] = {
- 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
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/Admin.scala b/src/main/scala/code/model/dataAccess/Admin.scala
index fce39e02b..8ad222ae9 100644
--- a/src/main/scala/code/model/dataAccess/Admin.scala
+++ b/src/main/scala/code/model/dataAccess/Admin.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
@@ -59,6 +59,7 @@ object Admin extends Admin with MetaMegaProtoUser[Admin]{
locale, timezone, password)
// comment this line out to require email validations
+ // TODO Get this from Props
override def skipEmailValidation = true
//Keep track of the referer on login
diff --git a/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala
new file mode 100644
index 000000000..0dde6471c
--- /dev/null
+++ b/src/main/scala/code/model/dataAccess/BankAccountCreationDispatcher.scala
@@ -0,0 +1,173 @@
+/**
+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
+
+ */
+
+
+/**
+* the message to be received in message queue
+* so that the API create an Bank (if necessary),
+* the bank account and an owner view.
+*/
+package com.tesobe.model {
+@SerialVersionUID(3988687883966746423L) case class CreateBankAccount (
+ accountOwnerId: String,
+ accountOwnerProvider: String,
+ accountNumber: String,
+ bankIdentifier: String,
+ bankName: String
+ )
+}
+
+package code.model.dataAccess {
+
+import code.bankconnectors.Connector
+import code.model._
+import code.users.Users
+import code.views.Views
+import com.rabbitmq.client.{ConnectionFactory,Channel}
+ import net.liftmodules.amqp.{
+ AMQPDispatcher,
+ AMQPMessage,
+ SerializedConsumer,
+ AMQPAddListener
+ }
+
+import net.liftweb.util._
+import net.liftweb.common.{Failure, Loggable, Full}
+import net.liftweb.actor.LiftActor
+import com.tesobe.model.{CreateBankAccount, UpdateBankAccount}
+
+
+/**
+ * an AMQP dispatcher that waits for message coming from a specif queue
+ * and dispatching them to the subscribed actors
+ */
+ class BankAccountCreationDispatcher[T](factory: ConnectionFactory)
+ extends AMQPDispatcher[T](factory) {
+ override def configure(channel: Channel) {
+ channel.exchangeDeclare("directExchange4", "direct", false)
+ channel.queueDeclare("createBankAccount", false, false, false, null)
+ channel.queueBind ("createBankAccount", "directExchange4", "createBankAccount")
+ channel.basicConsume("createBankAccount", false, new SerializedConsumer(channel, this))
+ }
+ }
+
+ object BankAccountCreation extends Loggable {
+
+ def setAsOwner(bankId : BankId, accountId : AccountId, user: User): Unit = {
+ createOwnerView(bankId, accountId, user)
+ Connector.connector.vend.setAccountHolder(BankAccountUID(bankId, accountId), user)
+ }
+
+
+ private def createOwnerView(bankId : BankId, accountId : AccountId, user: User): Unit = {
+
+ val ownerViewUID = ViewUID(ViewId("owner"), bankId, accountId)
+ val existingOwnerView = Views.views.vend.view(ownerViewUID)
+
+ existingOwnerView match {
+ case Full(v) => {
+ logger.info(s"account $accountId at bank $bankId has already an owner view")
+ v.users.toList.find(_.apiId == user.apiId) match {
+ case Some(u) => {
+ logger.info(s"user ${user.emailAddress} has already an owner view access on account $accountId at bank $bankId")
+ }
+ case _ =>{
+ //TODO: When can this case occur?
+ logger.info(s"creating owner view access to user ${user.emailAddress}")
+ Views.views.vend.addPermission(ownerViewUID, user)
+ }
+ }
+ }
+ case _ => {
+ {
+ //TODO: if we add more permissions to ViewImpl we need to remember to set them here...
+ logger.info(s"creating owner view on account account $accountId at bank $bankId")
+ val view = ViewImpl.createAndSaveOwnerView(bankId, accountId, "")
+
+ logger.info(s"creating owner view access to user ${user.emailAddress}")
+ Views.views.vend.addPermission(ownerViewUID, user)
+ }
+ }
+ }
+ }
+ }
+
+ object BankAccountCreationListener extends Loggable {
+
+ lazy val factory = new ConnectionFactory {
+ import ConnectionFactory._
+ setHost(Props.get("connection.host", "localhost"))
+ setPort(DEFAULT_AMQP_PORT)
+ setUsername(Props.get("connection.user", DEFAULT_USER))
+ setPassword(Props.get("connection.password", DEFAULT_PASS))
+ setVirtualHost(DEFAULT_VHOST)
+ }
+
+ val amqp = new BankAccountCreationDispatcher[CreateBankAccount](factory)
+
+ val createBankAccountListener = new LiftActor {
+ protected def messageHandler = {
+ case msg@AMQPMessage(message: CreateBankAccount) => {
+ logger.info(s"got message to create account/bank: ${message.accountNumber} / ${message.bankIdentifier}")
+
+ //TODO: Revise those dummy values
+ val accountType = "AMPQ"
+ val accountLabel = message.accountNumber
+ val currency = "EUR"
+
+ val foundUser = Users.users.vend.getUserByProviderId(message.accountOwnerProvider, message.accountOwnerId)
+ val result = for {
+ user <- foundUser ?~!
+ s"user ${message.accountOwnerId} at ${message.accountOwnerProvider} not found. Could not create the account with owner view"
+ } yield {
+ val (_, bankAccount) = Connector.connector.vend.createBankAndAccount(message.bankName, message.bankIdentifier, message.accountNumber, accountType, accountLabel, currency, user.name)
+ logger.info(s"created account with id ${bankAccount.bankId.value} with number ${bankAccount.number} at bank with identifier ${message.bankIdentifier}")
+ BankAccountCreation.setAsOwner(bankAccount.bankId, bankAccount.accountId, user)
+ }
+
+ result match {
+ case Full(_) =>
+ logger.info(s"Send message to get updates for the account with account number ${message.accountNumber} at ${message.bankIdentifier}")
+ UpdatesRequestSender.sendMsg(UpdateBankAccount(message.accountNumber, message.bankIdentifier))
+ case Failure(msg, _, _) => logger.warn(s"account creation failed: $msg")
+ case _ => logger.warn(s"account creation failed")
+ }
+
+ }
+ }
+ }
+ def startListen = {
+ logger.info("started to listen for bank account creation messages")
+ amqp ! AMQPAddListener(createBankAccountListener)
+ }
+ }
+}
\ No newline at end of file
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/model/dataAccess/MappedBank.scala b/src/main/scala/code/model/dataAccess/MappedBank.scala
index 0674d39e2..c600e50bd 100644
--- a/src/main/scala/code/model/dataAccess/MappedBank.scala
+++ b/src/main/scala/code/model/dataAccess/MappedBank.scala
@@ -11,19 +11,24 @@ 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)
+ override def bankId: BankId = BankId(permalink.get) // This is the bank id used in URLs
override def fullName: String = fullBankName.get
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
}
object MappedBank extends MappedBank with LongKeyedMetaMapper[MappedBank] {
+ // permalink should be unique
+ // TODO should have UniqueIndex on permalink but need to modify tests see createBank
+ // TODO Other Models should be able to foreign key to this but would need to expose IdPK then?
override def dbIndexes = Index(permalink) :: super.dbIndexes
def findByBankId(bankId : BankId) =
MappedBank.find(By(MappedBank.permalink, bankId.value))
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala
index 5d049f6d3..9ea05fa59 100644
--- a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala
+++ b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala
@@ -1,9 +1,9 @@
package code.model.dataAccess
-import java.math.MathContext
+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 {
@@ -20,20 +20,19 @@ 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)
object accountName extends MappedString(this, 255)
- object kind extends MappedString(this, 40)
+ object kind extends MappedString(this, 255) // This is the account type aka financial product name
+
+ //object productCode extends MappedString(this, 255)
+
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)
override def iban: Option[String] = {
val i = accountIban.get
@@ -49,8 +48,10 @@ class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccou
override def balance: BigDecimal = Helper.smallestCurrencyUnitToBigDecimal(accountBalance.get, currency)
override def name: String = accountName.get
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/main/scala/code/model/dataAccess/MappedKafkaBankAccountData.scala b/src/main/scala/code/model/dataAccess/MappedKafkaBankAccountData.scala
new file mode 100644
index 000000000..45d6c476d
--- /dev/null
+++ b/src/main/scala/code/model/dataAccess/MappedKafkaBankAccountData.scala
@@ -0,0 +1,25 @@
+package code.model.dataAccess
+
+import net.liftweb.mapper._
+
+class MappedKafkaBankAccountData extends LongKeyedMapper[MappedKafkaBankAccountData] with IdPK with CreatedUpdated {
+
+ override def getSingleton = MappedKafkaBankAccountData
+
+ object bankId extends MappedString(this, 255)
+ def getBankId = bankId.get
+ def setBankId(value: String) = bankId.set(value)
+
+ object accountId extends MappedString(this, 255)
+ def getAccountId = accountId.get
+ def setAccountId(value: String) = accountId.set(value)
+
+ object accountLabel extends MappedString(this, 255)
+ def getLabel = accountLabel.get
+ def setLabel(value: String) = accountLabel.set(value)
+
+}
+
+object MappedKafkaBankAccountData extends MappedKafkaBankAccountData with LongKeyedMetaMapper[MappedKafkaBankAccountData] {
+ override def dbIndexes = UniqueIndex(bankId, accountId) :: super.dbIndexes
+}
diff --git a/src/main/scala/code/model/dataAccess/MongoConfig.scala b/src/main/scala/code/model/dataAccess/MongoConfig.scala
index dd66201d3..48cde8daf 100644
--- a/src/main/scala/code/model/dataAccess/MongoConfig.scala
+++ b/src/main/scala/code/model/dataAccess/MongoConfig.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/main/scala/code/model/dataAccess/OBPTransaction.scala b/src/main/scala/code/model/dataAccess/OBPTransaction.scala
index 622051e44..70a3acd17 100644
--- a/src/main/scala/code/model/dataAccess/OBPTransaction.scala
+++ b/src/main/scala/code/model/dataAccess/OBPTransaction.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
@@ -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/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala
index f2dbb6c4b..59077416e 100755
--- a/src/main/scala/code/model/dataAccess/OBPUser.scala
+++ b/src/main/scala/code/model/dataAccess/OBPUser.scala
@@ -1,74 +1,108 @@
/**
-Open Bank Project - API
-Copyright (C) 2011, 2013, 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.model.dataAccess
-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 net.liftweb.http.{SessionVar,Templates}
-import code.model.User
-import net.liftweb.json.JsonDSL._
-import net.liftweb.http.SHtml
-import net.liftweb.http.S
-import com.mongodb.DBObject
-import net.liftweb.http.js.JsCmds.FocusOnLoad
+import java.util.UUID
+import code.api.{DirectLogin, OAuthHandshake}
+import code.bankconnectors.KafkaMappedConnector
+import code.bankconnectors.KafkaMappedConnector.KafkaInboundUser
+import net.liftweb.common._
+import net.liftweb.http.js.JsCmds.FocusOnLoad
+import net.liftweb.http.{S, SHtml, SessionVar, Templates}
+import net.liftweb.mapper._
+import net.liftweb.util.Mailer.{BCC, From, Subject, To}
+import net.liftweb.util._
+
+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{
+class OBPUser extends MegaProtoUser[OBPUser] with Logger {
def getSingleton = OBPUser // what's the "meta" server
object user extends MappedLongForeignKey(this, APIUser)
- def displayName() = {
- if(firstName.get.isEmpty) {
- lastName.get
- } else if(lastName.get.isEmpty) {
- firstName.get
+ /**
+ * The username field for the User.
+ */
+ lazy val username: userName = new userName()
+ class userName extends MappedString(this, 64) {
+ override def displayName = S.?("username")
+ override def dbIndexed_? = true
+ override def validations = valUnique(S.?("unique.username")) _ :: super.validations
+ override val fieldId = Some(Text("txtUsername"))
+ }
+
+
+ /**
+ * 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 getProvider() = {
+ if(provider.get == null) {
+ Props.get("hostname","")
+ } else if ( provider.get == "" || provider.get == Props.get("hostname","") ) {
+ Props.get("hostname","")
} else {
- firstName.get + " " + lastName.get
+ provider.get
}
}
def createUnsavedApiUser() : APIUser = {
APIUser.create
- .name_(displayName())
+ .name_(username)
.email(email)
- .provider_(Props.get("hostname",""))
- .providerId(email)
+ .provider_(getProvider())
+ .providerId(username)
+ }
+
+ def getApiUsersByEmail(userEmail: String) : List[APIUser] = {
+ APIUser.findAll(By(APIUser.email, userEmail))
+ }
+
+ def getApiUserByUsername(username: String) : Box[APIUser] = {
+ APIUser.find(By(APIUser.name_, username))
}
override def save(): Boolean = {
@@ -76,14 +110,15 @@ class OBPUser extends MegaProtoUser[OBPUser] with Logger{
info("user reference is null. We will create an API User")
val apiUser = createUnsavedApiUser()
apiUser.save()
- user(apiUser)
+ user(apiUser) //is this saving apiUser into a user field?
}
else {
- info("user reference is no null. Tying to update the API User")
+ info("user reference is not null. Trying to update the API User")
user.obj.map{ u =>{
info("API User found ")
- u.name_(displayName())
+ u.name_(username)
.email(email)
+ .providerId(username)
.save
}
}
@@ -95,6 +130,24 @@ class OBPUser extends MegaProtoUser[OBPUser] with Logger{
user.obj.map{_.delete_!}
super.delete_!
}
+
+ // Regex to validate an email address as per W3C recommendations: https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
+ private val emailRegex = """^[a-zA-Z0-9\.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r
+
+ def isEmailValid(e: String): Boolean = e match{
+ case null => false
+ case e if e.trim.isEmpty => false
+ case e if emailRegex.findFirstMatchIn(e).isDefined => true
+ case _ => false
+ }
+
+ // Override the validate method of MappedEmail class
+ // There's no way to override the default emailPattern from MappedEmail object
+ override lazy val email = new MyEmail(this, 48) {
+ override def validations = super.validations
+ override def dbIndexed_? = false
+ override def validate = if (isEmailValid(i_is_!)) Nil else List(FieldError(this, Text(S.?("invalid.email.address"))))
+ }
}
/**
@@ -109,45 +162,56 @@ 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 signupFields = List(firstName, lastName, email, password)
+ override def fieldOrder = List(id, firstName, lastName, email, username, password, provider)
+ override def signupFields = List(firstName, lastName, email, username, password)
// comment this line out to require email validations
override def skipEmailValidation = true
override def loginXhtml = {
- import net.liftweb.http.js.JsCmds.Noop
val loginXml = Templates(List("templates-hidden","_login")).map({
"form [action]" #> {S.uri} &
- "#loginText * " #> {S.??("log.in")} &
- "#emailAddressText * " #> {S.??("email.address")} &
- "#passwordText * " #> {S.??("password")} &
+ "#loginText * " #> {S.?("log.in")} &
+ "#usernameText * " #> {S.?("username")} &
+ "#passwordText * " #> {S.?("password")} &
"#recoverPasswordLink * " #> {
"a [href]" #> {lostPasswordPath.mkString("/", "/", "")} &
- "a *" #> {S.??("recover.password")}
+ "a *" #> {S.?("recover.password")}
} &
"#SignUpLink * " #> {
"a [href]" #> {OBPUser.signUpPath.foldLeft("")(_ + "/" + _)} &
- "a *" #> {S.??("sign.up")}
+ "a *" #> {S.?("sign.up")}
}
})
- SHtml.span(loginXml getOrElse NodeSeq.Empty,Noop)
+
+
{loginXml getOrElse NodeSeq.Empty}
}
/**
- * Overriden to use the hostname set in the props file
+ * Find current user
*/
- override def sendPasswordReset(email: String) {
- findUserByUserName(email) match {
+ def getCurrentUserUsername: String = {
+ if (OAuthHandshake.getUser.getOrElse(None) != None )
+ return OAuthHandshake.getUser.get.name
+ if (DirectLogin.getUser.getOrElse(None) != None)
+ return DirectLogin.getUser.get.name
+ return ""
+ }
+
+ /**
+ * Overridden to use the hostname set in the props file
+ */
+ override def sendPasswordReset(name: String) {
+ findUserByUsername(name) match {
case Full(user) if user.validated_? =>
user.resetUniqueId().save
val resetLink = Props.get("hostname", "ERROR")+
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)
@@ -161,6 +225,35 @@ import net.liftweb.util.Helpers._
}
}
+ override def lostPasswordXhtml = {
+
- This form is designed to allow developers to create test accounts. When an account is created,
- a view with VIEW_ID "owner" will be created, and access to this view will be granted to the user
- (you) who created it. If you wish, you may then populate the account with transactions using the
- v1.2.1 payments API.
- If you wish to do this, you will probably want to create some other accounts to send payments to and receive
- payments from.
-
-
- Create a new test bank account:
-
-
-
- Thank you, account ACCOUNT_ID has
- been created at bank BANK_ID with
- an initial balance of INITIAL_BALANCE
- CURRENCY.
-
-
+
+
+ This form is designed to allow developers to create test accounts. When an account is created,
+ a view with VIEW_ID "owner" will be created, and access to this view will be granted to the user
+ (you) who created it. If you wish, you may then populate the account with transactions using the
+ v1.2.1 payments API.
+ If you wish to do this, you will probably want to create some other accounts to send payments to and receive
+ payments from.
+
+
+ Create a new test bank account:
+
+
+
+ Thank you, account ACCOUNT_ID has
+ been created at bank BANK_ID with
+ an initial balance of INITIAL_BALANCE
+ CURRENCY.
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/favicon.ico b/src/main/webapp/favicon.ico
index f8fb81e31..29e667ad2 100644
Binary files a/src/main/webapp/favicon.ico and b/src/main/webapp/favicon.ico differ
diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
index 09cf87bcb..13ad54f81 100755
--- a/src/main/webapp/index.html
+++ b/src/main/webapp/index.html
@@ -1,6 +1,6 @@
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 this sandbox here.
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. Explore...
+
+
+
+
+
+
+
+
+
Transactions
+
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). Explore...
+
+
+
+
+
+
+
+
+
Counterparties
+
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. Explore...
+
+
+
+
+
+
+
+
+
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 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 Explore...
+
+
+
+
+
+
+
+
Payments & Transaction requests
+
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. Explore...
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.
+
+
+
+
+
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
+
+
+ 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
+
+
Double check parameters are spelt correctly (including http vs https etc.)
+
Check your encoding (use UTF8)
+
+
+
+
+
How can I use OAuth?
+
+ To get started with OBP and OAuth we recommend you use (and fork) one of our OAuth Starter SDKs.
+
+ Alternatively, for a walkthrough example with sample code, please see here. We use OAuth 1.0a. For deepish technical details of the flow see here
+ Note: You don't have to use OAuth for a hackathon - You can use Direct Login instead.
+
+
+
+
+
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!
+
+
+
+
+
+
+
+
+
+
Get started building your application using this sandbox now, register for a developer key
-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
--->
-
-
-
-
-
- Open Bank Project: %*%
-
-
-
-
-
-
-
-
-
Open Bank Project
-
-
-
-
-
-
-
Incorrect username or password
- The application web app wants
- to access to your data, please login
-
-
-
-
-
-
-
Please come back to the application where you come from and enter the following code:
-
123123213
-
-
Error message
-
-
-
+ The application web app is
+ asking for access to your account data.
+
+
+
+
+
Please go back to the application where you come from and enter the following code:
+
123123213
+
+
Error message
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/main/webapp/oauth/thanks.html b/src/main/webapp/oauth/thanks.html
index a9851d24b..a583c4972 100644
--- a/src/main/webapp/oauth/thanks.html
+++ b/src/main/webapp/oauth/thanks.html
@@ -1,28 +1,29 @@
@@ -39,9 +40,10 @@ Open Bank Project (http://www.openbankproject.com)