Initial Commit

master
rubenwardy 2016-11-19 17:57:09 +00:00
commit 58a3789ba9
15 changed files with 679 additions and 0 deletions

155
.gitignore vendored Normal file
View File

@ -0,0 +1,155 @@
# Created by https://www.gitignore.io/api/java,linux,osx,windows,gradle,intellij
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
# Sensitive or high-churn files:
.idea/dataSources/
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
### Java ###
*.class
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Gradle ###
.gradle
/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
/build

19
LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2016 rubenwardy
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

113
README.md Normal file
View File

@ -0,0 +1,113 @@
# MonzoAPI Retrofit
License: MIT.
Created by rubenwardy
## Installation
Coming soon, I'm not very good with Java Libraries.
## Aquiring a Retrofit Interface
Use MonzoAPIBuilder to create MonzoAPI instances.
```Java
MonzoAPI monzo_unauthorized = MonzoAPIBuilder.createService();
MonzoAPI monzo_authorized = MonzoAPIBuilder.createService("accesstoken");
```
MonzoAPI is an interface which specifies MonzoAPI endpoints.
## Endpoints
### Exchange Auth Token for AccessToken
Get an access token. The first parameter must be "authorization_code".
```Java
monzo_unauthorized.getAccessTokenFromAuthCode("authorization_code", BuildConfig.CLIENT_ID,
BuildConfig.CLIENT_SECRET, redirectUri, code).enqueue(new Callback<AccessToken>() {
@Override
public void onResponse(Call<AccessToken> call, Response<AccessToken> response) {
AccessToken token = response.body();
if (token != null) {
// success
} else {
// failure
}
}
@Override
public void onFailure(Call<AccessToken> call, Throwable t) {
// failure
}
});
```
### Getting list of accounts
```Java
monzo_authorized.getAccounts().enqueue(new Callback<MonzoAPIService.AccountList>() {
@Override
public void onResponse(Call<MonzoAPIService.AccountList> call, Response<MonzoAPIService.AccountList> response) {
MonzoAPIService.AccountList accountList = response.body();
if (accountList != null && accountList.accounts.size() > 0) {
List<Account> accounts = accountList.accounts;
// success
} else {
// failure
}
}
@Override
public void onFailure(Call<MonzoAPIService.AccountList> call, Throwable t) {
// failure
}
});
```
### Getting transactions
The second parameter must be "merchant" - the future we'll make this optional.
```Java
monzo_authorized.getTransactions(account_id, "merchant").enqueue(new Callback<MonzoAPIService.TransactionList>() {
@Override
public void onResponse(Call<MonzoAPIService.TransactionList> call, Response<MonzoAPIService.TransactionList> response) {
if (response.body() != null) {
List<Transactions> full_transactions = response.body().transactions;
// success
} else {
// failure
}
}
@Override
public void onFailure(Call<MonzoAPIService.TransactionList> call, Throwable t) {
// failure
}
});
```
### Get Balance
```Java
monzo_authorized.getBalance(account_id).enqueue(new Callback<Balance>() {
@Override
public void onResponse(Call<Balance> call, Response<Balance> response) {
Balance balance = response.body();
if (balance != null) {
// success
} else {
// failure
}
}
@Override
public void onFailure(Call<Balance> call, Throwable t) {
// failure
}
});
```

10
build.gradle Normal file
View File

@ -0,0 +1,10 @@
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

36
monzo_retrofit.iml Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":monzo_retrofit" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":monzo_retrofit" />
</configuration>
</facet>
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="true" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="retrofit-2.1.0" level="project" />
<orderEntry type="library" exported="" name="converter-gson-2.1.0" level="project" />
<orderEntry type="library" exported="" name="okhttp-3.3.0" level="project" />
<orderEntry type="library" exported="" name="gson-2.7" level="project" />
<orderEntry type="library" exported="" name="okio-1.8.0" level="project" />
</component>
</module>

View File

@ -0,0 +1,14 @@
package com.rubenwardy.monzo_retrofit;
public class AccessToken {
public String access_token;
public String client_id;
public int expires_in;
public String refresh_token;
public String token_type;
public String user_id;
public boolean isValid() {
return access_token != null && access_token.length() >= 10;
}
}

View File

@ -0,0 +1,13 @@
package com.rubenwardy.monzo_retrofit;
import java.util.Date;
public class Account {
public String id;
public String description;
public Date created;
public String toString() {
return id + " / " + description + " / " + created;
}
}

View File

@ -0,0 +1,7 @@
package com.rubenwardy.monzo_retrofit;
public class Balance {
public int balance;
public String currency;
public int spend_today;
}

View File

@ -0,0 +1,31 @@
package com.rubenwardy.monzo_retrofit;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
class DateDeserializer implements JsonDeserializer<Date> {
private DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS'Z'");
public DateDeserializer() {
df.setTimeZone(TimeZone.getTimeZone("GMT"));
}
@Override
public Date deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
try {
return df.parse(json.getAsString());
} catch (ParseException e) {
return null;
}
}
}

View File

@ -0,0 +1,22 @@
package com.rubenwardy.monzo_retrofit;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
class DeclineReasonDeserializer implements JsonDeserializer<Transaction.DeclineReason> {
@Override
public Transaction.DeclineReason deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
Transaction.DeclineReason[] scopes = Transaction.DeclineReason.values();
for (Transaction.DeclineReason scope : scopes) {
if (scope.toString().equals(json.getAsString())) {
return scope;
}
}
return null;
}
}

View File

@ -0,0 +1,87 @@
package com.rubenwardy.monzo_retrofit;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Created by ruben on 17/11/16.
*/
public class DummyTransactionBuilder {
List<Merchant> merchants = new ArrayList<>();
private void addMerchant(Merchant merchant) {
merchant.id = merchant.name;
merchant.group_id = "main";
merchant.created = new Date();
merchant.updated = merchant.created;
merchant.metadata = new HashMap<>();
merchant.emoji = "";
merchant.logo = "";
merchants.add(merchant);
}
private void fillMeta() {
merchants.clear();
Merchant merchant = new Merchant();
merchant.name = "Bakery";
merchant.category = "eating_out";
merchant.online = false;
merchant.abm = false;
addMerchant(merchant);
}
public List<Transaction> build() {
fillMeta();
List<Transaction> transactions = new ArrayList<>();
Calendar calendar = GregorianCalendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.MONTH, -2);
Date earliest = calendar.getTime();
Date date = new Date();
int balance = 20;
while (date.getTime() > earliest.getTime()) {
{
int cost = ThreadLocalRandom.current().nextInt(500, 2000 + 1);
Transaction transaction = new Transaction();
transaction.id = date.getTime() + ":" + cost + ":" + balance;
transaction.account_balance = balance;
transaction.amount = -cost;
transaction.created = new Date(date.getTime());
transaction.merchant = merchants.get(0);
balance += cost;
transactions.add(transaction);
}
if (balance > 10000) {
Transaction transaction = new Transaction();
transaction.id = date.getTime() + ":" + 10000 + ":" + balance;
transaction.account_balance = balance;
transaction.amount = 10000;
transaction.created = new Date(date.getTime());
transaction.merchant = merchants.get(0);
balance -= 10000;
}
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_YEAR, -1);
date = calendar.getTime();
}
Collections.reverse(transactions);
return transactions;
}
}

View File

@ -0,0 +1,26 @@
package com.rubenwardy.monzo_retrofit;
import java.util.Date;
import java.util.Map;
public class Merchant {
public String id;
public String group_id;
public Date created;
public String name;
public String logo;
public String emoji;
public String category;
public boolean online;
public boolean abm;
public Date updated;
public Map<String, String> metadata;
public String getVenueType() {
if (metadata.containsKey("foursquare_category")) {
return metadata.get("foursquare_category");
} else {
return "other";
}
}
}

View File

@ -0,0 +1,36 @@
package com.rubenwardy.monzo_retrofit;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface MonzoAPI {
@FormUrlEncoded
@POST("oauth2/token")
Call<AccessToken> getAccessTokenFromAuthCode(
@Field("grant_type") String grant_type,
@Field("client_id") String client_id,
@Field("client_secret") String client_secret,
@Field("redirect_uri") String redirect_uri,
@Field("code") String code);
@GET("accounts")
Call<AccountList> getAccounts();
class AccountList {
public List<Account> accounts;
}
@GET("balance")
Call<Balance> getBalance(@Query("account_id") String account_id);
@GET("transactions")
Call<TransactionList> getTransactions(@Query("account_id") String account_id, @Query("expand[]") String expand);
class TransactionList {
public List<Transaction> transactions;
}
}

View File

@ -0,0 +1,53 @@
package com.rubenwardy.monzo_retrofit;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.util.Date;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MonzoAPIBuilder {
public static String API_BASE_URL = "https://api.monzo.com/";
public static String USER_AUTH_URL = "https://auth.getmondo.co.uk/";
private static Retrofit.Builder createBaseBuilder() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
gsonBuilder.registerTypeAdapter(DeclineReasonDeserializer.class, new DeclineReasonDeserializer());
return new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gsonBuilder.create()));
}
public static MonzoAPI createService() {
return createBaseBuilder().build().create(MonzoAPI.class);
}
public static MonzoAPI createService(final String accesstoken) {
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder()
.header("Accept", "application/json")
.header("Authorization",
"Bearer " + accesstoken)
.method(original.method(), original.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
}).build();
return createBaseBuilder().client(client).build().create(MonzoAPI.class);
}
}

View File

@ -0,0 +1,57 @@
package com.rubenwardy.monzo_retrofit;
import java.util.Date;
import java.util.Map;
public class Transaction {
public int account_balance;
public int amount;
public String currency;
public String description;
public String id;
public String notes;
public enum DeclineReason {
NOT_DECLINED,
INSUFFICIENT_FUNDS,
CARD_INACTIVE,
CARD_BLOCKED,
OTHER;
public String toString() {
switch (this) {
case NOT_DECLINED:
return "NOT_DECLINED";
case INSUFFICIENT_FUNDS:
return "INSUFFICIENT_FUNDS";
case CARD_INACTIVE:
return "CARD_INACTIVE";
case CARD_BLOCKED:
return "CARD_BLOCKED";
default:
return "OTHER";
}
}
}
// This is only present on declined transactions
public DeclineReason decline_reason = DeclineReason.NOT_DECLINED;
// Top-ups to an account are represented as transactions with a positive amount and is_load = true.
// Other transactions such as refunds, reversals or chargebacks may have a positive amount
// but is_load = false
public boolean is_load;
public Date created;
// The timestamp at which the transaction settled. In most cases, this happens 24-48 hours after
// created. If this field is not present, the transaction is authorised but not yet “complete.”
public Date settled;
public String category;
// This contains the merchant_id of the merchant that this transaction was made at.
public Merchant merchant;
public Map<String, String> metadata;
}