Merge pull request #149 from pbozin/develop

Switch to using unique username instead of email
This commit is contained in:
pbozin 2016-09-07 09:35:46 +00:00 committed by GitHub
commit 769c82a280
225 changed files with 43456 additions and 2531 deletions

2
.gitignore vendored
View File

@ -12,4 +12,4 @@
target
src/main/resources/
src/test/resources/
*.iml
*.iml

32
CONTRIBUTING.md Normal file
View File

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

View File

@ -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.
This work is licensed under a Creative Commons Attribution 3.0 Unported License.

14
NOTICE
View File

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

113
README.md
View File

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

7
mvn.sh Executable file
View File

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

171
pom.xml
View File

@ -11,9 +11,9 @@
<name>Open Bank Project API</name>
<inceptionYear>2011</inceptionYear>
<properties>
<scala.version>2.10</scala.version>
<scala.compiler>2.10.4</scala.compiler>
<lift.version>2.6-M4</lift.version>
<scala.version>2.11</scala.version>
<scala.compiler>2.11.8</scala.compiler>
<lift.version>2.6.3</lift.version>
<!-- Common plugin settings -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>${project.build.sourceEncoding}</project.reporting.outputEncoding>
@ -27,12 +27,12 @@
<repository>
<id>scala-tools.releases</id>
<name>Scala-Tools Dependencies Repository for Releases</name>
<url>https://oss.sonatype.org/content/groups/scala-tools/</url>
<url>https://oss.sonatype.org/content/repositories/releases/</url>
</repository>
<repository>
<id>java.net.maven2</id>
<name>java.net Maven2 Repository</name>
<url>http://download.java.net/maven/2/</url>
<id>java.net.maven3</id>
<name>java.net Maven3 Repository</name>
<url>http://download.java.net/maven/3/</url>
</repository>
<repository>
<id>scala-tools.snapshots</id>
@ -50,6 +50,11 @@
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-xml_${scala.version}</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>net.liftweb</groupId>
<artifactId>lift-mapper_${scala.version}</artifactId>
@ -58,17 +63,17 @@
<dependency>
<groupId>net.databinder.dispatch</groupId>
<artifactId>dispatch-lift-json_${scala.version}</artifactId>
<version>0.10.1</version>
<version>0.11.3</version>
</dependency>
<dependency>
<groupId>net.databinder.dispatch</groupId>
<artifactId>dispatch-core_${scala.version}</artifactId>
<version>0.10.1</version>
<version>0.11.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.9</version>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_${scala.version}</artifactId>
<version>0.10.0.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
@ -76,44 +81,50 @@
<version>1.46</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4-701.jdbc4</version>
<version>9.4-1206-jdbc4</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.2.138</version>
<version>1.4.191</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.version}</artifactId>
<version>2.0</version>
<version>2.2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>7.6.16.v20140903</version>
<version>9.2.15.v20160210</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>7.6.16.v20140903</version>
<version>9.2.15.v20160210</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.manub</groupId>
<artifactId>scalatest-embedded-kafka_${scala.version}</artifactId>
<version>0.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
@ -129,17 +140,74 @@
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>2.35.0</version>
<version>2.52.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-htmlunit-driver</artifactId>
<version>2.52.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.21</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.2.0</version>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>net.liftmodules</groupId>
<artifactId>amqp_2.6_${scala.version}</artifactId>
<version>1.3</version>
<version>1.4-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.pegdown</groupId>
<artifactId>pegdown</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>com.jason-goodwin</groupId>
<artifactId>authentikat-jwt_${scala.version}</artifactId>
<version>0.4.1</version>
</dependency>
<dependency>
<groupId>com.tokbox</groupId>
<artifactId>opentok-server-sdk</artifactId>
<version>3.0.0-beta.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sksamuel.elastic4s</groupId>
<artifactId>elastic4s-core_2.11</artifactId>
<version>2.3.0</version>
</dependency>
<!-- for LiftConsole -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-compiler</artifactId>
<version>${scala.compiler}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.compiler}</version>
</dependency>
<dependency>
<groupId>oauth.signpost</groupId>
<artifactId>signpost-commonshttp4</artifactId>
<version>1.2.1.2</version>
</dependency>
</dependencies>
@ -150,7 +218,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.9</version>
<version>${scala.version}</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
@ -159,12 +227,13 @@
<!-- enable the scalatest plugin -->
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0-RC1</version>
<version>1.0</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<forkMode>once</forkMode>
<junitxml>.</junitxml>
<filereports>WDF TestSuite.txt</filereports>
<argLine>-Drun.mode=test</argLine>
<argLine>-Drun.mode=test -XX:MaxPermSize=128m -Xms512m -Xmx512m</argLine>
</configuration>
<executions>
<execution>
@ -179,7 +248,7 @@
<!-- add src/main/java to source dirs -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.9.1</version>
<version>1.10</version>
<executions>
<execution>
<phase>generate-sources</phase>
@ -197,14 +266,16 @@
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.0</version>
<version>3.2.2</version>
<configuration>
<scalaVersion>${scala.compiler}</scalaVersion>
<charset>${project.build.sourceEncoding}</charset>
<jvmArgs>
<jvmArg>-Xmx1024m</jvmArg>
<jvmArg>-DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties</jvmArg>
</jvmArgs>
<scalaVersion>${scala.compiler}</scalaVersion>
<scalaCompatVersion>${scala.version}</scalaCompatVersion>
<recompileMode>incremental</recompileMode>
<useZincServer>true</useZincServer>
</configuration>
<executions>
<execution>
@ -212,18 +283,33 @@
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
<configuration>
<args>
<arg>-dependencyfile</arg>
<arg>${project.build.directory}/.scala_dependencies</arg>
<arg>-Xmax-classfile-name</arg>
<arg>78</arg>
</args>
</configuration>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<version>3.0.1</version>
<executions>
<execution>
<id>default-copy-resources</id>
@ -250,16 +336,17 @@
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.9</version>
<version>6.1.26</version>
<configuration>
<contextPath>/</contextPath>
<scanIntervalSeconds>5</scanIntervalSeconds>
<jvmArgs>-Xmx512m -Xms512m</jvmArgs>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>yuicompressor-maven-plugin</artifactId>
<version>1.3.2</version>
<version>1.5.1</version>
<executions>
<execution>
<goals>
@ -275,7 +362,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-idea-plugin</artifactId>
<version>2.2</version>
<version>2.2.1</version>
<configuration>
<downloadSources>true</downloadSources>
</configuration>
@ -283,7 +370,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.7</version>
<version>2.10</version>
<configuration>
<downloadSources>true</downloadSources>
<additionalProjectnatures>
@ -301,7 +388,7 @@
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.1.0</version>
<version>2.2.1</version>
<executions>
<execution>
<goals>
@ -323,14 +410,16 @@
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.0</version>
<version>3.2.2</version>
<configuration>
<scalaVersion>${scala.compiler}</scalaVersion>
<charset>${project.build.sourceEncoding}</charset>
<jvmArgs>
<jvmArg>-Xmx1024m</jvmArg>
<jvmArg>-DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties</jvmArg>
</jvmArgs>
<scalaCompatVersion>${scala.version}</scalaCompatVersion>
<scalaVersion>${scala.compiler}</scalaVersion>
<recompileMode>incremental</recompileMode>
<useZincServer>true</useZincServer>
</configuration>
</plugin>
</plugins>

View File

@ -1,2 +1,2 @@
#Project properties
sbt.version=0.11.3
sbt.version=0.13.9

View File

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

View File

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

View File

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

View File

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

View File

@ -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
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 = <p class="about-text"> \
Welcome to the API Sandbox powered by the Open Bank Project! <br/> \
</p>
# 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
|<ul>
|<li> operation_id is concatenation of version and function and should be unque (the aim of this is to allow links to code) </li>
|<li> version references the version that the API call is defined in.</li>
|<li> function is the (scala) function.</li>
|<li> request_url is empty for the root call, else the path.</li>
|<li> summary is a short description inline with the swagger terminology. </li>
|<li> description may contain html markup (generated from markdown on the server).</li>
|</ul>
""",
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))
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (<em>))
// 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
)
*/
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]
}
/**
* \
*
* @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
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package code.customerinfo
package code.customer
import java.util.Date

View File

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

View File

@ -1,4 +1,4 @@
package code.customerinfo
package code.customer
import java.util.Date

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) _
)
}
}
case class AmountOfMoney (
val currency: String,
val amount: String
)

View File

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

View File

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

View File

@ -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 =
<lift:crud.all>
<div>
<p>
Total of {Consumer.count} applications from {recordsWithUniqueEmails.getOrElse("ERROR")} unique email addresses.
{recordsWithUniqueAppNames.getOrElse("ERROR")} unique app names.
</p>
<br/>
<br/>
<br/>
</div>
<p id="admin-consumer-summary">
Total of {Consumer.count} applications from {recordsWithUniqueEmails.getOrElse("ERROR")} unique email addresses. <br />
{recordsWithUniqueAppNames.getOrElse("ERROR")} unique app names.
</p>
<table id={showAllId} class={showAllClass}>
<thead>
<tr>
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More