monzo-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +5 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +102 -0
- data/README.md +64 -0
- data/Rakefile +5 -0
- data/bin/monzo-cli +115 -0
- data/extras/header.jpg +0 -0
- data/features/monzo.feature +8 -0
- data/features/step_definitions/monzo_steps.rb +6 -0
- data/features/support/env.rb +15 -0
- data/lib/api/mondo/account.rb +12 -0
- data/lib/api/mondo/address.rb +6 -0
- data/lib/api/mondo/attachment.rb +26 -0
- data/lib/api/mondo/balance.rb +15 -0
- data/lib/api/mondo/card.rb +26 -0
- data/lib/api/mondo/client.rb +236 -0
- data/lib/api/mondo/errors.rb +50 -0
- data/lib/api/mondo/feed_item.rb +31 -0
- data/lib/api/mondo/merchant.rb +15 -0
- data/lib/api/mondo/resource.rb +52 -0
- data/lib/api/mondo/response.rb +29 -0
- data/lib/api/mondo/transaction.rb +80 -0
- data/lib/api/mondo/utils.rb +16 -0
- data/lib/api/mondo/version.rb +3 -0
- data/lib/api/mondo/web_hook.rb +15 -0
- data/lib/api/mondo.rb +21 -0
- data/lib/monzo/config_parser.rb +31 -0
- data/lib/monzo/monzo_api.rb +27 -0
- data/lib/monzo/version.rb +3 -0
- data/lib/monzo.rb +4 -0
- data/monzo.gemspec +36 -0
- data/spec/assets/bad_test_config.yml +2 -0
- data/spec/assets/test_config.yml +3 -0
- data/spec/config_parser_spec.rb +48 -0
- data/spec/monzo_api_spec.rb +22 -0
- data/spec/spec_helper.rb +25 -0
- metadata +236 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Mondo
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class ApiError < Error
|
8
|
+
attr_reader :response, :code, :description
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
@response = response
|
12
|
+
@code = response.status
|
13
|
+
|
14
|
+
puts response.inspect
|
15
|
+
|
16
|
+
begin
|
17
|
+
parsed_response = MultiJson.decode(response.body)
|
18
|
+
errors = parsed_response["message"]
|
19
|
+
@description = stringify_errors(errors)
|
20
|
+
rescue MultiJson::ParseError
|
21
|
+
@description = response.body ? response.body.strip : "Unknown error"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"#{super}. #{self.description}"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def stringify_errors(errors)
|
32
|
+
case errors
|
33
|
+
when Array
|
34
|
+
errors.join(", ")
|
35
|
+
when Hash
|
36
|
+
errors.flat_map do |field, messages|
|
37
|
+
messages.map { |message| "#{field} #{message}" }
|
38
|
+
end.join(", ")
|
39
|
+
else
|
40
|
+
errors.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ClientError < Error
|
46
|
+
end
|
47
|
+
|
48
|
+
class SignatureError < Error
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mondo
|
2
|
+
class FeedItem < Resource
|
3
|
+
|
4
|
+
attr_accessor :title,
|
5
|
+
:image_url,
|
6
|
+
:url, # TODO - make me consistent when the Mondo API changes
|
7
|
+
:background_color,
|
8
|
+
:body,
|
9
|
+
:type
|
10
|
+
|
11
|
+
|
12
|
+
# temporary fix until the API accepts JSON data in the request body
|
13
|
+
def save
|
14
|
+
self.client.api_post("/feed", self.create_params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_params
|
18
|
+
{
|
19
|
+
account_id: self.client.account_id,
|
20
|
+
type: "basic",
|
21
|
+
url: self.url,
|
22
|
+
params: {
|
23
|
+
title: self.title,
|
24
|
+
image_url: self.image_url,
|
25
|
+
background_color: self.background_color,
|
26
|
+
body: self.body
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mondo
|
2
|
+
class Merchant < Resource
|
3
|
+
|
4
|
+
attr_accessor :id, :group_id, :logo, :name, :raw_data, :address, :emoji
|
5
|
+
|
6
|
+
boolean_accessor :online, :is_load, :settled
|
7
|
+
|
8
|
+
date_accessor :created
|
9
|
+
|
10
|
+
def address
|
11
|
+
::Mondo::Address.new(raw_data['address'], self.client)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Mondo
|
2
|
+
class Resource
|
3
|
+
|
4
|
+
attr_accessor :client, :raw_data
|
5
|
+
|
6
|
+
def initialize(hash={}, client)
|
7
|
+
self.raw_data = hash
|
8
|
+
self.client = client
|
9
|
+
hash.each { |key,val| send("#{key}=", val) if respond_to?("#{key}=") }
|
10
|
+
self.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#<#{self.class} #{raw_data}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
self.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def date_writer(*args)
|
23
|
+
args.each do |attr|
|
24
|
+
define_method("#{attr.to_s}=".to_sym) do |date|
|
25
|
+
date = (date.is_a?(String) ? DateTime.parse(date) : date) rescue date
|
26
|
+
instance_variable_set("@#{attr}", date)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def date_accessor(*args)
|
32
|
+
attr_reader(*args)
|
33
|
+
date_writer(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def boolean_accessor(*attrs)
|
37
|
+
attr_accessor(*attrs)
|
38
|
+
alias_question(attrs)
|
39
|
+
end
|
40
|
+
|
41
|
+
def boolean_reader(*attrs)
|
42
|
+
attr_reader(*attrs)
|
43
|
+
alias_question(attrs)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def alias_question(attrs)
|
48
|
+
attrs.each{ |attr| define_method("#{attr}?"){ send(attr) || false } }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Mondo
|
2
|
+
class Response
|
3
|
+
attr_reader :response, :parsed
|
4
|
+
attr_accessor :error, :options
|
5
|
+
|
6
|
+
# Initializes a Response instance
|
7
|
+
#
|
8
|
+
# @param [Faraday::Response] response The Faraday response instance
|
9
|
+
def initialize(resp)
|
10
|
+
@response = resp
|
11
|
+
@parsed = -> { MultiJson.load(body) rescue body }.call
|
12
|
+
end
|
13
|
+
|
14
|
+
# The HTTP response headers
|
15
|
+
def headers
|
16
|
+
response.headers
|
17
|
+
end
|
18
|
+
|
19
|
+
# The HTTP response status code
|
20
|
+
def status
|
21
|
+
response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
# The HTTP response body
|
25
|
+
def body
|
26
|
+
response.body || ''
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Mondo
|
2
|
+
class Transaction < Resource
|
3
|
+
|
4
|
+
attr_accessor :id,
|
5
|
+
:description,
|
6
|
+
:notes,
|
7
|
+
:metadata,
|
8
|
+
:is_load,
|
9
|
+
:category,
|
10
|
+
:settled,
|
11
|
+
:decline_reason
|
12
|
+
|
13
|
+
date_accessor :created
|
14
|
+
date_accessor :settled
|
15
|
+
|
16
|
+
def declined?
|
17
|
+
raw_data['decline_reason'].present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def amount
|
21
|
+
Money.new(raw_data['amount'], currency)
|
22
|
+
end
|
23
|
+
|
24
|
+
def local_amount
|
25
|
+
Money.new(raw_data['local_amount'], local_currency)
|
26
|
+
end
|
27
|
+
|
28
|
+
def account_balance
|
29
|
+
Money.new(raw_data['account_balance'], currency)
|
30
|
+
end
|
31
|
+
|
32
|
+
def currency
|
33
|
+
Money::Currency.new(raw_data['currency'])
|
34
|
+
end
|
35
|
+
|
36
|
+
def local_currency
|
37
|
+
Money::Currency.new(raw_data['local_currency'])
|
38
|
+
end
|
39
|
+
|
40
|
+
def save_metadata
|
41
|
+
self.client.api_patch("/transactions/#{self.id}", metadata: self.metadata)
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_attachment(args={})
|
45
|
+
attachment = Attachment.new(
|
46
|
+
{
|
47
|
+
external_id: self.id,
|
48
|
+
file_url: args.fetch(:file_url),
|
49
|
+
file_type: args.fetch(:file_type)
|
50
|
+
},
|
51
|
+
self.client
|
52
|
+
)
|
53
|
+
|
54
|
+
self.attachments << attachment if attachment.register
|
55
|
+
end
|
56
|
+
|
57
|
+
def attachments
|
58
|
+
@transactions ||= begin
|
59
|
+
raw_data['attachments'].map { |tx| Attachment.new(tx, self.client) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def merchant(opts={})
|
64
|
+
unless raw_data['merchant'].kind_of?(Hash)
|
65
|
+
# Go and refetch the transaction with merchant info expanded
|
66
|
+
self.raw_data['merchant'] = self.client.transaction(self.id, expand: [:merchant]).raw_data['merchant']
|
67
|
+
end
|
68
|
+
|
69
|
+
::Mondo::Merchant.new(raw_data['merchant'], client) unless raw_data['merchant'].nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
def tags
|
73
|
+
metadata["tags"]
|
74
|
+
end
|
75
|
+
|
76
|
+
def tags=(t)
|
77
|
+
metadata["tags"] = t
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Mondo
|
4
|
+
module Utils
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# String Helpers
|
8
|
+
def camelize(str)
|
9
|
+
str.split('_').map(&:capitalize).join
|
10
|
+
end
|
11
|
+
|
12
|
+
def underscore(str)
|
13
|
+
str.gsub(/(.)([A-Z])/) { "#{$1}_#{$2.downcase}" }.downcase
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mondo
|
2
|
+
class WebHook < Resource
|
3
|
+
|
4
|
+
attr_accessor :id, :account_id, :url
|
5
|
+
|
6
|
+
def save
|
7
|
+
self.client.api_post("webhooks", account_id: account_id, url: url)
|
8
|
+
end
|
9
|
+
|
10
|
+
def delete
|
11
|
+
self.client.api_delete("webhooks/#{self.id}")
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/api/mondo.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mondo
|
2
|
+
require_relative '../api/mondo/client'
|
3
|
+
require_relative '../api/mondo/version'
|
4
|
+
|
5
|
+
require_relative '../api/mondo/response'
|
6
|
+
require_relative '../api/mondo/resource'
|
7
|
+
require_relative '../api/mondo/errors'
|
8
|
+
require_relative '../api/mondo/utils'
|
9
|
+
|
10
|
+
require_relative '../api/mondo/transaction'
|
11
|
+
require_relative '../api/mondo/feed_item'
|
12
|
+
|
13
|
+
require_relative '../api/mondo/card'
|
14
|
+
require_relative '../api/mondo/balance'
|
15
|
+
require_relative '../api/mondo/address'
|
16
|
+
require_relative '../api/mondo/transaction'
|
17
|
+
require_relative '../api/mondo/account'
|
18
|
+
require_relative '../api/mondo/merchant'
|
19
|
+
require_relative '../api/mondo/attachment'
|
20
|
+
require_relative '../api/mondo/web_hook'
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class ConfigParser
|
4
|
+
|
5
|
+
def initialize(path_to_config)
|
6
|
+
@path_to_config = path_to_config
|
7
|
+
@parsed = nil
|
8
|
+
|
9
|
+
@parsed = begin
|
10
|
+
YAML.load(File.open(@path_to_config))
|
11
|
+
rescue ArgumentError => e
|
12
|
+
puts "Could not parse YAML: #{e.message}"
|
13
|
+
end if File.exist?(@path_to_config)
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
return nil unless valid?
|
19
|
+
@parsed
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
return nil if @parsed.nil?
|
24
|
+
return nil if @parsed['access_token'].nil?
|
25
|
+
return nil if @parsed['account_id'].nil?
|
26
|
+
return nil if @parsed['user_id'].nil?
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative '../api/mondo'
|
2
|
+
|
3
|
+
class MonzoApi
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@user_id = config.parse['user_id']
|
7
|
+
@account_id = config.parse['account_id']
|
8
|
+
@access_token = config.parse['access_token']
|
9
|
+
|
10
|
+
@monzo = Mondo::Client.new(
|
11
|
+
token: @access_token,
|
12
|
+
account_id: @user_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def balance
|
16
|
+
@monzo.balance @account_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def accounts
|
20
|
+
@monzo.accounts
|
21
|
+
end
|
22
|
+
|
23
|
+
def transactions
|
24
|
+
@monzo.transactions
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/monzo.rb
ADDED
data/monzo.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Ensure we require the local version and not one we might have installed already
|
2
|
+
require File.join([File.dirname(__FILE__), 'lib', 'monzo', 'version.rb'])
|
3
|
+
spec = Gem::Specification.new do |s|
|
4
|
+
s.name = 'monzo-cli'
|
5
|
+
s.version = Monzo::VERSION
|
6
|
+
s.author = 'cesar ferreira'
|
7
|
+
s.email = 'cesar.manuel.ferreira@gmail.com'
|
8
|
+
s.homepage = 'http://cesarferreira.com'
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.summary = 'A description of your project'
|
11
|
+
s.files = `git ls-files`.split("
|
12
|
+
")
|
13
|
+
s.require_paths << 'lib'
|
14
|
+
s.required_ruby_version = '>= 2.0.0'
|
15
|
+
s.bindir = 'bin'
|
16
|
+
s.executables << 'monzo-cli'
|
17
|
+
|
18
|
+
s.add_development_dependency 'rake'
|
19
|
+
s.add_development_dependency 'aruba'
|
20
|
+
s.add_development_dependency 'rspec'
|
21
|
+
s.add_development_dependency 'pry'
|
22
|
+
|
23
|
+
s.add_runtime_dependency 'gli', '2.14.0'
|
24
|
+
s.add_runtime_dependency 'terminal-table', '>= 1.7.3'
|
25
|
+
# s.add_runtime_dependency 'mondo', '~> 0.5.0'
|
26
|
+
s.add_runtime_dependency 'colorize', '~> 0.7'
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
s.add_runtime_dependency 'oauth2', '~> 1.0'
|
31
|
+
s.add_runtime_dependency 'money'
|
32
|
+
s.add_runtime_dependency 'multi_json', '~> 1.10'
|
33
|
+
s.add_runtime_dependency 'activesupport', '~> 3.2'
|
34
|
+
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monzo/config_parser'
|
3
|
+
|
4
|
+
describe '# Config Parser' do
|
5
|
+
|
6
|
+
it '# The file is present' do
|
7
|
+
|
8
|
+
# Given
|
9
|
+
path = 'spec/assets/test_config.yml'
|
10
|
+
|
11
|
+
# When
|
12
|
+
config = ConfigParser.new(path)
|
13
|
+
|
14
|
+
# Then
|
15
|
+
expect(config.parse).to_not be_nil
|
16
|
+
expect(config.parse['access_token']).to eq('Qnjdas8hakxdjasQscGVgnVGIVXpvpZ5uCxkQ5XLnDHnOPoBtXreQ6adBo')
|
17
|
+
expect(config.parse['account_id']).to eq('acc_0aksdaklsjSh28181')
|
18
|
+
expect(config.parse['user_id']).to eq('user_18231092askdas9212')
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
it '# The file is not present' do
|
23
|
+
|
24
|
+
# Given
|
25
|
+
path = 'spec/assets/NOT_CORRECT/test_config.yml'
|
26
|
+
|
27
|
+
# When
|
28
|
+
config = ConfigParser.new(path)
|
29
|
+
|
30
|
+
# Then
|
31
|
+
expect(config.parse).to be_nil
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
it '# The file is present but is corrupt' do
|
36
|
+
|
37
|
+
# Given
|
38
|
+
path = 'spec/assets/bad_test_config.yml'
|
39
|
+
|
40
|
+
# When
|
41
|
+
config = ConfigParser.new(path)
|
42
|
+
|
43
|
+
# Then
|
44
|
+
expect(config.parse).to be_nil
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monzo/monzo_api'
|
3
|
+
|
4
|
+
describe '# Monzo API' do
|
5
|
+
|
6
|
+
# it '# The file is present' do
|
7
|
+
#
|
8
|
+
# # Given
|
9
|
+
# hash = 'spec/assets/test_config.yml'
|
10
|
+
#
|
11
|
+
# # When
|
12
|
+
# config = MonzoApi.new(hash)
|
13
|
+
#
|
14
|
+
# # Then
|
15
|
+
# expect(config.parse['access_token']).to eq('Qnjdas8hakxdjasQscGVgnVGIVXpvpZ5uCxkQ5XLnDHnOPoBtXreQ6adBo')
|
16
|
+
# expect(config.parse['account_id']).to eq('acc_0aksdaklsjSh28181')
|
17
|
+
# expect(config.parse['user_id']).to eq('user_18231092askdas9212')
|
18
|
+
#
|
19
|
+
# end
|
20
|
+
# expect { ... }.to raise_error
|
21
|
+
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'monzo'
|
2
|
+
RSpec.configure do |config|
|
3
|
+
# rspec-expectations config goes here. You can use an alternate
|
4
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
5
|
+
# assertions if you prefer.
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
8
|
+
# and `failure_message` of custom matchers include text for helper methods
|
9
|
+
# defined using `chain`, e.g.:
|
10
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
11
|
+
# # => "be bigger than 2 and smaller than 4"
|
12
|
+
# ...rather than:
|
13
|
+
# # => "be bigger than 2"
|
14
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
15
|
+
end
|
16
|
+
|
17
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
18
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
19
|
+
config.mock_with :rspec do |mocks|
|
20
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
21
|
+
# a real object. This is generally recommended, and will default to
|
22
|
+
# `true` in RSpec 4.
|
23
|
+
mocks.verify_partial_doubles = true
|
24
|
+
end
|
25
|
+
end
|